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.
Files changed (88) hide show
  1. package/bin/windowpp.js +86 -0
  2. package/cmake/embed_assets.py +144 -0
  3. package/framework/CMakeLists.txt +176 -0
  4. package/framework/include/windowpp/windowpp.h +704 -0
  5. package/framework/src/AppData/API/AppData.ts +137 -0
  6. package/framework/src/AppData/appdata_bridge.h +138 -0
  7. package/framework/src/AppData/appdata_manager.cpp +126 -0
  8. package/framework/src/AppData/appdata_manager.h +3 -0
  9. package/framework/src/FileSystem/API/FileSystem.ts +389 -0
  10. package/framework/src/FileSystem/Linux/filesearch.cpp +148 -0
  11. package/framework/src/FileSystem/Linux/readfile.cpp +79 -0
  12. package/framework/src/FileSystem/Linux/savefile.cpp +333 -0
  13. package/framework/src/FileSystem/MacOS/filesearch.cpp +149 -0
  14. package/framework/src/FileSystem/MacOS/readfile.cpp +80 -0
  15. package/framework/src/FileSystem/MacOS/savefile.cpp +264 -0
  16. package/framework/src/FileSystem/Windows/filesearch.cpp +195 -0
  17. package/framework/src/FileSystem/Windows/readfile.cpp +122 -0
  18. package/framework/src/FileSystem/Windows/savefile.cpp +290 -0
  19. package/framework/src/FileSystem/file_index_service.cpp +262 -0
  20. package/framework/src/FileSystem/file_index_service.h +55 -0
  21. package/framework/src/FileSystem/filesystem_bridge.h +243 -0
  22. package/framework/src/FileSystem/filesystem_handler.h +93 -0
  23. package/framework/src/FileSystem/filesystem_json.h +241 -0
  24. package/framework/src/FileSystem/filesystem_search_service.cpp +414 -0
  25. package/framework/src/FileSystem/filesystem_search_service.h +94 -0
  26. package/framework/src/Input/API/Input.ts +161 -0
  27. package/framework/src/Input/Linux/linux_key_utils.h +135 -0
  28. package/framework/src/Input/MacOS/macos_key_utils.h +137 -0
  29. package/framework/src/Input/Windows/win32_key_utils.h +199 -0
  30. package/framework/src/Input/input_bridge.h +192 -0
  31. package/framework/src/Input/input_service.cpp +584 -0
  32. package/framework/src/Input/input_service.h +21 -0
  33. package/framework/src/application.cpp +29 -0
  34. package/framework/src/common/hit_test.cpp +40 -0
  35. package/framework/src/common/image_loader.cpp +24 -0
  36. package/framework/src/common/paths.cpp +75 -0
  37. package/framework/src/filedrop/filedrop.cpp +316 -0
  38. package/framework/src/filedrop/filedrop.css +421 -0
  39. package/framework/src/filedrop/filedrop.hpp +92 -0
  40. package/framework/src/filedrop/filedrop.ts +183 -0
  41. package/framework/src/platform/API/App.ts +156 -0
  42. package/framework/src/platform/API/Window.ts +249 -0
  43. package/framework/src/platform/linux/app_linux.cpp +256 -0
  44. package/framework/src/platform/linux/app_linux.h +64 -0
  45. package/framework/src/platform/linux/linux_helpers.cpp +26 -0
  46. package/framework/src/platform/linux/linux_helpers.h +19 -0
  47. package/framework/src/platform/linux/tray_linux.cpp +21 -0
  48. package/framework/src/platform/linux/tray_linux.h +26 -0
  49. package/framework/src/platform/linux/window_linux.cpp +256 -0
  50. package/framework/src/platform/linux/window_linux.h +70 -0
  51. package/framework/src/platform/macos/app_macos.h +59 -0
  52. package/framework/src/platform/macos/app_macos.mm +223 -0
  53. package/framework/src/platform/macos/macos_helpers.h +21 -0
  54. package/framework/src/platform/macos/tray_macos.h +22 -0
  55. package/framework/src/platform/macos/tray_macos.mm +53 -0
  56. package/framework/src/platform/macos/window_macos.h +74 -0
  57. package/framework/src/platform/macos/window_macos.mm +318 -0
  58. package/framework/src/platform/platform_bridge.h +514 -0
  59. package/framework/src/platform/platform_factory.cpp +33 -0
  60. package/framework/src/platform/platform_factory.h +19 -0
  61. package/framework/src/platform/win32/app_win32.cpp +572 -0
  62. package/framework/src/platform/win32/app_win32.h +83 -0
  63. package/framework/src/platform/win32/tray_win32.cpp +57 -0
  64. package/framework/src/platform/win32/tray_win32.h +30 -0
  65. package/framework/src/platform/win32/win32_helpers.h +61 -0
  66. package/framework/src/platform/win32/window_win32.cpp +267 -0
  67. package/framework/src/platform/win32/window_win32.h +79 -0
  68. package/framework/src/renderer/webgpu.h +128 -0
  69. package/framework/src/renderer/webview/include/WebView2.h +48014 -0
  70. package/framework/src/renderer/webview/include/WebView2EnvironmentOptions.h +342 -0
  71. package/framework/src/renderer/webview/webview.h +13 -0
  72. package/framework/src/renderer/webview/webview_linux.cpp +392 -0
  73. package/framework/src/renderer/webview/webview_macos.mm +388 -0
  74. package/framework/src/renderer/webview/webview_win32.cpp +688 -0
  75. package/framework/src/renderer/webview/x64/WebView2Loader.dll +0 -0
  76. package/framework/src/renderer/webview/x64/WebView2Loader.lib +0 -0
  77. package/framework/src/renderer/webview/x64/WebView2LoaderStatic.lib +0 -0
  78. package/lib/build.js +112 -0
  79. package/lib/create.js +283 -0
  80. package/lib/dev.js +155 -0
  81. package/package.json +24 -0
  82. package/scripts/publish.js +67 -0
  83. package/scripts/sync-framework.js +73 -0
  84. package/templates/solid/CMakeLists.txt +56 -0
  85. package/templates/solid/frontend/package.json +22 -0
  86. package/templates/solid/frontend/vite.config.ts +25 -0
  87. package/templates/solid/main.cpp +72 -0
  88. package/templates/solid/package.json +12 -0
@@ -0,0 +1,290 @@
1
+ // ============================================================================
2
+ // FileSystem/Windows/savefile.cpp
3
+ // Handles: fs:writeFile, fs:writeBinaryFile, fs:appendFile,
4
+ // fs:copyFile, fs:moveFile, fs:deleteFile,
5
+ // fs:createDir, fs:removeDir
6
+ // ============================================================================
7
+
8
+ #define WIN32_LEAN_AND_MEAN
9
+ #define NOMINMAX
10
+ #include <windows.h>
11
+ #include <shellapi.h>
12
+
13
+ #include <algorithm>
14
+ #include <filesystem>
15
+ #include <fstream>
16
+ #include <set>
17
+ #include <stdexcept>
18
+ #include <string>
19
+ #include <vector>
20
+
21
+ #include "../filesystem_handler.h"
22
+
23
+ namespace wpp::fs {
24
+
25
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
26
+
27
+ static std::wstring toWide(const std::string& utf8) {
28
+ if (utf8.empty()) return {};
29
+ int len = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0);
30
+ std::wstring ws(len, L'\0');
31
+ MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, ws.data(), len);
32
+ if (!ws.empty() && ws.back() == L'\0') ws.pop_back();
33
+ return ws;
34
+ }
35
+
36
+ static std::string toUtf8(const std::wstring& ws) {
37
+ if (ws.empty()) return {};
38
+ int len = WideCharToMultiByte(CP_UTF8, 0, ws.c_str(), -1,
39
+ nullptr, 0, nullptr, nullptr);
40
+ std::string s(len, '\0');
41
+ WideCharToMultiByte(CP_UTF8, 0, ws.c_str(), -1,
42
+ s.data(), len, nullptr, nullptr);
43
+ if (!s.empty() && s.back() == '\0') s.pop_back();
44
+ return s;
45
+ }
46
+
47
+ static std::string toLowerAscii(std::string value) {
48
+ std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
49
+ return static_cast<char>(std::tolower(c));
50
+ });
51
+ return value;
52
+ }
53
+
54
+ static void writeBytes(const std::string& path,
55
+ const void* data, size_t size, bool append) {
56
+ std::wstring wpath = toWide(path);
57
+ DWORD access = GENERIC_WRITE;
58
+ DWORD creation = append ? OPEN_ALWAYS : CREATE_ALWAYS;
59
+
60
+ HANDLE hFile = CreateFileW(wpath.c_str(), access, 0, nullptr,
61
+ creation, FILE_ATTRIBUTE_NORMAL, nullptr);
62
+ if (hFile == INVALID_HANDLE_VALUE) {
63
+ DWORD err = GetLastError();
64
+ throw std::runtime_error("Cannot open '" + path + "' for writing (Win32 error " +
65
+ std::to_string(err) + ")");
66
+ }
67
+ if (append) {
68
+ if (SetFilePointer(hFile, 0, nullptr, FILE_END) == INVALID_SET_FILE_POINTER) {
69
+ CloseHandle(hFile);
70
+ throw std::runtime_error("Seek to end failed for '" + path + "'");
71
+ }
72
+ }
73
+ DWORD written = 0;
74
+ if (!WriteFile(hFile, data, static_cast<DWORD>(size), &written, nullptr) ||
75
+ written != static_cast<DWORD>(size)) {
76
+ CloseHandle(hFile);
77
+ throw std::runtime_error("Write failed for '" + path + "'");
78
+ }
79
+ CloseHandle(hFile);
80
+ }
81
+
82
+ static std::vector<uint8_t> base64Decode(const std::string& b64) {
83
+ static const int kDecTable[256] = {
84
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
85
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
86
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,
87
+ 52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1,
88
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,
89
+ 15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,
90
+ -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,
91
+ 41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1,
92
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
93
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
94
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
95
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
96
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
97
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
98
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
99
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
100
+ };
101
+
102
+ std::vector<uint8_t> out;
103
+ out.reserve(b64.size() * 3 / 4);
104
+ uint32_t buf = 0;
105
+ int bits = 0;
106
+ for (unsigned char c : b64) {
107
+ if (c == '=') break;
108
+ int v = kDecTable[c];
109
+ if (v < 0) continue;
110
+ buf = (buf << 6) | static_cast<uint32_t>(v);
111
+ bits += 6;
112
+ if (bits >= 8) {
113
+ bits -= 8;
114
+ out.push_back(static_cast<uint8_t>((buf >> bits) & 0xFF));
115
+ }
116
+ }
117
+ return out;
118
+ }
119
+
120
+ // ─── fs:writeFile ─────────────────────────────────────────────────────────────
121
+
122
+ void handleWriteFile(const std::string& path, const std::string& text) {
123
+ writeBytes(path, text.data(), text.size(), /*append=*/false);
124
+ }
125
+
126
+ // ─── fs:writeBinaryFile ───────────────────────────────────────────────────────
127
+
128
+ void handleWriteBinaryFile(const std::string& path, const std::string& b64) {
129
+ auto bytes = base64Decode(b64);
130
+ writeBytes(path, bytes.data(), bytes.size(), /*append=*/false);
131
+ }
132
+
133
+ // ─── fs:appendFile ────────────────────────────────────────────────────────────
134
+
135
+ void handleAppendFile(const std::string& path, const std::string& text) {
136
+ writeBytes(path, text.data(), text.size(), /*append=*/true);
137
+ }
138
+
139
+ // ─── fs:copyFile ─────────────────────────────────────────────────────────────
140
+
141
+ void handleCopyFile(const std::string& src, const std::string& dest) {
142
+ if (!CopyFileW(toWide(src).c_str(), toWide(dest).c_str(), /*bFailIfExists=*/FALSE)) {
143
+ DWORD err = GetLastError();
144
+ throw std::runtime_error("CopyFile failed: " + std::to_string(err));
145
+ }
146
+ }
147
+
148
+ // ─── fs:moveFile ─────────────────────────────────────────────────────────────
149
+
150
+ void handleMoveFile(const std::string& src, const std::string& dest) {
151
+ if (!MoveFileExW(toWide(src).c_str(), toWide(dest).c_str(),
152
+ MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED)) {
153
+ DWORD err = GetLastError();
154
+ throw std::runtime_error("MoveFile failed: " + std::to_string(err));
155
+ }
156
+ }
157
+
158
+ // ─── fs:deleteFile ────────────────────────────────────────────────────────────
159
+
160
+ void handleDeleteFile(const std::string& path) {
161
+ if (!DeleteFileW(toWide(path).c_str())) {
162
+ DWORD err = GetLastError();
163
+ throw std::runtime_error("DeleteFile failed: " + std::to_string(err));
164
+ }
165
+ }
166
+
167
+ // ─── fs:createDir ────────────────────────────────────────────────────────────
168
+
169
+ void handleCreateDir(const std::string& path, bool recursive) {
170
+ namespace fs = std::filesystem;
171
+ std::error_code ec;
172
+ if (recursive) {
173
+ fs::create_directories(fs::path(toWide(path)), ec);
174
+ } else {
175
+ fs::create_directory(fs::path(toWide(path)), ec);
176
+ }
177
+ if (ec) throw std::runtime_error("createDir failed: " + ec.message());
178
+ }
179
+
180
+ // ─── fs:removeDir ────────────────────────────────────────────────────────────
181
+
182
+ void handleRemoveDir(const std::string& path, bool recursive) {
183
+ namespace fs = std::filesystem;
184
+ std::error_code ec;
185
+ if (recursive) {
186
+ fs::remove_all(fs::path(toWide(path)), ec);
187
+ } else {
188
+ fs::remove(fs::path(toWide(path)), ec);
189
+ }
190
+ if (ec) throw std::runtime_error("removeDir failed: " + ec.message());
191
+ }
192
+
193
+ void handleOpenPath(const std::string& path) {
194
+ namespace fs = std::filesystem;
195
+ std::error_code ec;
196
+ if (!fs::exists(fs::path(toWide(path)), ec) || ec) {
197
+ throw std::runtime_error("openPath failed: path does not exist: " + path);
198
+ }
199
+
200
+ const auto result = reinterpret_cast<intptr_t>(
201
+ ShellExecuteW(nullptr, L"open", toWide(path).c_str(), nullptr, nullptr, SW_SHOWNORMAL));
202
+ if (result <= 32) {
203
+ throw std::runtime_error("openPath failed for '" + path + "' (ShellExecute error " +
204
+ std::to_string(result) + ")");
205
+ }
206
+ }
207
+
208
+ void handleRevealPath(const std::string& path) {
209
+ namespace fs = std::filesystem;
210
+ std::error_code ec;
211
+ fs::path target = fs::path(toWide(path));
212
+ if (!fs::exists(target, ec) || ec) {
213
+ throw std::runtime_error("revealPath failed: path does not exist: " + path);
214
+ }
215
+
216
+ if (fs::is_directory(target, ec)) {
217
+ handleOpenPath(path);
218
+ return;
219
+ }
220
+
221
+ const std::wstring params = L"/select,\"" + target.wstring() + L"\"";
222
+ const auto result = reinterpret_cast<intptr_t>(
223
+ ShellExecuteW(nullptr, L"open", L"explorer.exe", params.c_str(), nullptr, SW_SHOWNORMAL));
224
+ if (result <= 32) {
225
+ throw std::runtime_error("revealPath failed for '" + path + "' (ShellExecute error " +
226
+ std::to_string(result) + ")");
227
+ }
228
+ }
229
+
230
+ std::vector<ApplicationEntry> handleListApplications() {
231
+ namespace fs = std::filesystem;
232
+
233
+ std::vector<fs::path> roots;
234
+ if (const wchar_t* appdata = _wgetenv(L"APPDATA")) {
235
+ roots.emplace_back(std::wstring(appdata) + L"\\Microsoft\\Windows\\Start Menu\\Programs");
236
+ }
237
+ if (const wchar_t* program_data = _wgetenv(L"ProgramData")) {
238
+ roots.emplace_back(std::wstring(program_data) + L"\\Microsoft\\Windows\\Start Menu\\Programs");
239
+ }
240
+
241
+ std::vector<ApplicationEntry> apps;
242
+ std::set<std::string> seen;
243
+ for (const auto& root : roots) {
244
+ std::error_code ec;
245
+ if (!fs::exists(root, ec) || ec) {
246
+ continue;
247
+ }
248
+
249
+ fs::recursive_directory_iterator it(root, fs::directory_options::skip_permission_denied, ec);
250
+ fs::recursive_directory_iterator end;
251
+ for (; it != end; it.increment(ec)) {
252
+ if (ec) {
253
+ ec.clear();
254
+ continue;
255
+ }
256
+
257
+ const auto& entry = *it;
258
+ if (!entry.is_regular_file(ec) || ec) {
259
+ ec.clear();
260
+ continue;
261
+ }
262
+
263
+ const std::string extension = toLowerAscii(entry.path().extension().u8string());
264
+ if (extension != ".lnk" && extension != ".url" && extension != ".appref-ms" && extension != ".exe") {
265
+ continue;
266
+ }
267
+
268
+ const std::string path = entry.path().u8string();
269
+ const std::string dedupe_key = toLowerAscii(path);
270
+ if (!seen.insert(dedupe_key).second) {
271
+ continue;
272
+ }
273
+
274
+ apps.push_back(ApplicationEntry{
275
+ path,
276
+ entry.path().stem().u8string(),
277
+ path,
278
+ path,
279
+ });
280
+ }
281
+ }
282
+
283
+ return apps;
284
+ }
285
+
286
+ void handleLaunchApplication(const std::string& target) {
287
+ handleOpenPath(target);
288
+ }
289
+
290
+ } // namespace wpp::fs
@@ -0,0 +1,262 @@
1
+ #include "file_index_service.h"
2
+
3
+ #include "filesystem_handler.h"
4
+
5
+ #include <nlohmann/json.hpp>
6
+
7
+ #include <filesystem>
8
+ #include <functional>
9
+
10
+ #ifdef _WIN32
11
+ #include <windows.h>
12
+ #endif
13
+
14
+ namespace wpp::fs {
15
+
16
+ namespace {
17
+
18
+ using NativeJson = nlohmann::json;
19
+
20
+ std::string normalize_path(const std::string& value) {
21
+ return std::filesystem::path(value).lexically_normal().u8string();
22
+ }
23
+
24
+ bool is_hidden_entry(const std::filesystem::directory_entry& entry) {
25
+ const auto filename = entry.path().filename().u8string();
26
+ if (!filename.empty() && filename[0] == '.') {
27
+ return true;
28
+ }
29
+
30
+ #ifdef _WIN32
31
+ const DWORD attributes = GetFileAttributesW(entry.path().c_str());
32
+ return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_HIDDEN) != 0;
33
+ #else
34
+ return false;
35
+ #endif
36
+ }
37
+
38
+ int64_t path_modified_ms(const std::string& path) {
39
+ try {
40
+ const auto stat = handleStat(path);
41
+ return stat.modifiedMs;
42
+ } catch (...) {
43
+ return -1;
44
+ }
45
+ }
46
+
47
+ NativeJson entry_to_json(const IndexedPathEntry& entry) {
48
+ return NativeJson{
49
+ {"path", entry.path},
50
+ {"name", entry.name},
51
+ {"isDir", entry.isDir},
52
+ {"isHidden", entry.isHidden},
53
+ };
54
+ }
55
+
56
+ IndexedPathEntry entry_from_json(const NativeJson& value) {
57
+ IndexedPathEntry entry;
58
+ entry.path = value.value("path", "");
59
+ entry.name = value.value("name", "");
60
+ entry.isDir = value.value("isDir", false);
61
+ entry.isHidden = value.value("isHidden", false);
62
+ return entry;
63
+ }
64
+
65
+ } // namespace
66
+
67
+ FileIndexService& FileIndexService::instance() {
68
+ static FileIndexService service;
69
+ return service;
70
+ }
71
+
72
+ void FileIndexService::configure_cache_root(const std::string& cache_root) {
73
+ std::lock_guard<std::mutex> lock(mutex_);
74
+ cache_root_ = cache_root;
75
+ if (!cache_root_.empty()) {
76
+ std::error_code ec;
77
+ std::filesystem::create_directories(std::filesystem::u8path(cache_root_), ec);
78
+ }
79
+ }
80
+
81
+ std::vector<IndexedPathEntry> FileIndexService::get_or_build_root_index(const std::string& root,
82
+ std::atomic<bool>* cancelled,
83
+ uint64_t& scanned_entries) {
84
+ scanned_entries = 0;
85
+ auto state = get_state(root);
86
+
87
+ std::lock_guard<std::mutex> lock(state->mutex);
88
+ if (!state->loaded && load_from_disk(state)) {
89
+ state->loaded = true;
90
+ }
91
+
92
+ const int64_t current_root_modified = path_modified_ms(state->root);
93
+ if (state->loaded && state->rootModifiedMs >= 0 && current_root_modified == state->rootModifiedMs) {
94
+ scanned_entries = static_cast<uint64_t>(state->entries.size());
95
+ return state->entries;
96
+ }
97
+
98
+ int64_t built_root_modified = current_root_modified;
99
+ auto rebuilt = build_root_index(state->root, cancelled, scanned_entries, built_root_modified);
100
+ if (cancelled && cancelled->load()) {
101
+ return {};
102
+ }
103
+
104
+ state->entries = std::move(rebuilt);
105
+ state->rootModifiedMs = built_root_modified;
106
+ state->loaded = true;
107
+ save_to_disk(*state);
108
+ return state->entries;
109
+ }
110
+
111
+ std::shared_ptr<FileIndexService::RootIndex> FileIndexService::get_state(const std::string& root) {
112
+ const std::string normalized = normalize_root(root);
113
+ std::lock_guard<std::mutex> lock(mutex_);
114
+ auto it = indexes_.find(normalized);
115
+ if (it != indexes_.end()) {
116
+ return it->second;
117
+ }
118
+
119
+ auto state = std::make_shared<RootIndex>();
120
+ state->root = normalized;
121
+ indexes_.emplace(normalized, state);
122
+ return state;
123
+ }
124
+
125
+ std::vector<IndexedPathEntry> FileIndexService::build_root_index(const std::string& root,
126
+ std::atomic<bool>* cancelled,
127
+ uint64_t& scanned_entries,
128
+ int64_t& root_modified_ms) const {
129
+ std::vector<IndexedPathEntry> entries;
130
+ root_modified_ms = path_modified_ms(root);
131
+
132
+ std::error_code ec;
133
+ std::filesystem::path root_path(root);
134
+ if (std::filesystem::is_regular_file(root_path, ec)) {
135
+ entries.push_back(IndexedPathEntry{
136
+ normalize_path(root),
137
+ root_path.filename().u8string(),
138
+ false,
139
+ false,
140
+ });
141
+ scanned_entries = 1;
142
+ return entries;
143
+ }
144
+
145
+ if (ec || !std::filesystem::is_directory(root_path, ec)) {
146
+ return entries;
147
+ }
148
+
149
+ std::vector<std::filesystem::path> pending_dirs;
150
+ pending_dirs.push_back(root_path);
151
+
152
+ while (!pending_dirs.empty()) {
153
+ if (cancelled && cancelled->load()) {
154
+ return {};
155
+ }
156
+
157
+ std::filesystem::path dir = std::move(pending_dirs.back());
158
+ pending_dirs.pop_back();
159
+
160
+ auto it = std::filesystem::directory_iterator(
161
+ dir,
162
+ std::filesystem::directory_options::skip_permission_denied,
163
+ ec);
164
+ if (ec) {
165
+ continue;
166
+ }
167
+
168
+ for (const auto& entry : it) {
169
+ if (cancelled && cancelled->load()) {
170
+ return {};
171
+ }
172
+
173
+ ++scanned_entries;
174
+ const bool hidden = is_hidden_entry(entry);
175
+ const bool is_directory = entry.is_directory(ec);
176
+ ec.clear();
177
+
178
+ entries.push_back(IndexedPathEntry{
179
+ normalize_path(entry.path().u8string()),
180
+ entry.path().filename().u8string(),
181
+ is_directory,
182
+ hidden,
183
+ });
184
+
185
+ if (is_directory) {
186
+ pending_dirs.push_back(entry.path());
187
+ }
188
+ }
189
+ }
190
+
191
+ return entries;
192
+ }
193
+
194
+ bool FileIndexService::load_from_disk(const std::shared_ptr<RootIndex>& state) {
195
+ const std::string cache_file = cache_file_for_root(state->root);
196
+ if (cache_file.empty() || !handleExists(cache_file)) {
197
+ return false;
198
+ }
199
+
200
+ try {
201
+ const auto parsed = NativeJson::parse(handleReadFile(cache_file));
202
+ if (parsed.value("version", 0) != 1) {
203
+ return false;
204
+ }
205
+
206
+ if (parsed.value("root", std::string()) != state->root) {
207
+ return false;
208
+ }
209
+
210
+ state->rootModifiedMs = parsed.value("rootModifiedMs", -1LL);
211
+ state->entries.clear();
212
+ for (const auto& entry : parsed.value("entries", NativeJson::array())) {
213
+ state->entries.push_back(entry_from_json(entry));
214
+ }
215
+ return true;
216
+ } catch (...) {
217
+ return false;
218
+ }
219
+ }
220
+
221
+ void FileIndexService::save_to_disk(const RootIndex& state) const {
222
+ const std::string cache_file = cache_file_for_root(state.root);
223
+ if (cache_file.empty()) {
224
+ return;
225
+ }
226
+
227
+ NativeJson entries = NativeJson::array();
228
+ for (const auto& entry : state.entries) {
229
+ entries.push_back(entry_to_json(entry));
230
+ }
231
+
232
+ NativeJson payload{
233
+ {"version", 1},
234
+ {"root", state.root},
235
+ {"rootModifiedMs", state.rootModifiedMs},
236
+ {"entries", std::move(entries)},
237
+ };
238
+
239
+ const auto parent = std::filesystem::u8path(cache_file).parent_path();
240
+ if (!parent.empty()) {
241
+ std::error_code ec;
242
+ std::filesystem::create_directories(parent, ec);
243
+ }
244
+
245
+ handleWriteFile(cache_file, payload.dump());
246
+ }
247
+
248
+ std::string FileIndexService::normalize_root(const std::string& root) const {
249
+ return normalize_path(root);
250
+ }
251
+
252
+ std::string FileIndexService::cache_file_for_root(const std::string& root) const {
253
+ std::lock_guard<std::mutex> lock(mutex_);
254
+ if (cache_root_.empty()) {
255
+ return {};
256
+ }
257
+
258
+ const size_t digest = std::hash<std::string>{}(root);
259
+ return (std::filesystem::u8path(cache_root_) / (std::to_string(digest) + ".json")).u8string();
260
+ }
261
+
262
+ } // namespace wpp::fs
@@ -0,0 +1,55 @@
1
+ #pragma once
2
+
3
+ #include <atomic>
4
+ #include <memory>
5
+ #include <mutex>
6
+ #include <string>
7
+ #include <unordered_map>
8
+ #include <vector>
9
+
10
+ namespace wpp::fs {
11
+
12
+ struct IndexedPathEntry {
13
+ std::string path;
14
+ std::string name;
15
+ bool isDir = false;
16
+ bool isHidden = false;
17
+ };
18
+
19
+ class FileIndexService {
20
+ public:
21
+ static FileIndexService& instance();
22
+
23
+ void configure_cache_root(const std::string& cache_root);
24
+
25
+ std::vector<IndexedPathEntry> get_or_build_root_index(const std::string& root,
26
+ std::atomic<bool>* cancelled,
27
+ uint64_t& scanned_entries);
28
+
29
+ private:
30
+ struct RootIndex {
31
+ std::string root;
32
+ int64_t rootModifiedMs = -1;
33
+ std::vector<IndexedPathEntry> entries;
34
+ bool loaded = false;
35
+ std::mutex mutex;
36
+ };
37
+
38
+ mutable std::mutex mutex_;
39
+ std::string cache_root_;
40
+ std::unordered_map<std::string, std::shared_ptr<RootIndex>> indexes_;
41
+
42
+ FileIndexService() = default;
43
+
44
+ std::shared_ptr<RootIndex> get_state(const std::string& root);
45
+ std::vector<IndexedPathEntry> build_root_index(const std::string& root,
46
+ std::atomic<bool>* cancelled,
47
+ uint64_t& scanned_entries,
48
+ int64_t& root_modified_ms) const;
49
+ bool load_from_disk(const std::shared_ptr<RootIndex>& state);
50
+ void save_to_disk(const RootIndex& state) const;
51
+ std::string normalize_root(const std::string& root) const;
52
+ std::string cache_file_for_root(const std::string& root) const;
53
+ };
54
+
55
+ } // namespace wpp::fs