windowpp 0.1.2 → 0.1.4

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 (30) hide show
  1. package/bin/windowpp.js +4 -1
  2. package/lib/create.js +63 -2
  3. package/package.json +4 -2
  4. package/scripts/publish.js +4 -0
  5. package/scripts/sync-templates.js +238 -0
  6. package/templates/example/CMakeLists.txt +59 -0
  7. package/templates/example/frontend/index.html +20 -0
  8. package/templates/example/frontend/src/API.ts +56 -0
  9. package/templates/example/frontend/src/App.tsx +781 -0
  10. package/templates/example/frontend/src/Layout.tsx +5 -0
  11. package/templates/example/frontend/src/components/ClipboardToast.tsx +23 -0
  12. package/templates/example/frontend/src/components/FileSearch.tsx +936 -0
  13. package/templates/example/frontend/src/components/InfiniteScrollList.tsx +267 -0
  14. package/templates/example/frontend/src/components/index.ts +13 -0
  15. package/templates/example/frontend/src/filedrop.css +421 -0
  16. package/templates/example/frontend/src/index.css +1 -0
  17. package/templates/example/frontend/src/index.tsx +24 -0
  18. package/templates/example/frontend/src/pages/About.tsx +47 -0
  19. package/templates/example/frontend/src/pages/Settings.tsx +37 -0
  20. package/templates/example/frontend/tsconfig.json +20 -0
  21. package/templates/example/frontend/vite.config.ts +27 -0
  22. package/templates/example/main.cpp +224 -0
  23. package/templates/example/package.json +12 -0
  24. package/templates/solid/CMakeLists.txt +4 -1
  25. package/templates/solid/frontend/index.html +16 -0
  26. package/templates/solid/frontend/src/App.tsx +8 -0
  27. package/templates/solid/frontend/src/index.css +1 -0
  28. package/templates/solid/frontend/src/index.tsx +12 -0
  29. package/templates/solid/frontend/tsconfig.json +15 -0
  30. package/templates/solid/frontend/vite.config.ts +3 -1
@@ -0,0 +1,781 @@
1
+ import { For, Show, createSignal, onMount, onCleanup } from 'solid-js';
2
+ import { A } from '@solidjs/router';
3
+ import {
4
+ appData, type AppDataEntry,
5
+ filesystem,
6
+ fileDrop, formatFileSize, type FileInfo,
7
+ input, type InputKeyEvent, type ShortcutEvent,
8
+ appApi, type AppInfo, type MonitorInfo,
9
+ windowApi, type WindowEvent, type WindowInfo,
10
+ } from './API';
11
+ import './filedrop.css';
12
+ import {
13
+ ClipboardToast,
14
+ FileSearch,
15
+ FileSearchButton,
16
+ PresetsPanel,
17
+ type FileSearchResult,
18
+ type SearchPreset,
19
+ } from './components';
20
+
21
+ // ============================================================================
22
+ // Utilities
23
+ // ============================================================================
24
+
25
+ function normalizePath(path: string): string {
26
+ return path.replace(/\\/g, '/').replace(/\/+/g, '/');
27
+ }
28
+
29
+ function inferSearchPresets(appDataRoot: string): SearchPreset[] {
30
+ const presets: SearchPreset[] = [];
31
+
32
+ if (appDataRoot) {
33
+ presets.push({
34
+ label: 'App Data',
35
+ path: appDataRoot,
36
+ note: 'Small, safe default for quick testing',
37
+ });
38
+ }
39
+
40
+ const normalized = normalizePath(appDataRoot);
41
+
42
+ if (/^[A-Za-z]:\//.test(normalized)) {
43
+ const driveRoot = normalized.slice(0, 3);
44
+ const usersIndex = normalized.toLowerCase().indexOf('/appdata/');
45
+ if (usersIndex > 3) {
46
+ presets.push({
47
+ label: 'Home',
48
+ path: normalized.slice(0, usersIndex).replace(/\//g, '\\'),
49
+ note: 'User profile on Windows',
50
+ });
51
+ }
52
+ presets.push({
53
+ label: 'Drive Root',
54
+ path: driveRoot.replace(/\//g, '\\'),
55
+ note: 'Broadest Windows search scope',
56
+ });
57
+ return dedupePresets(presets);
58
+ }
59
+
60
+ const macIndex = normalized.indexOf('/Library/Application Support/');
61
+ if (macIndex > 0) {
62
+ presets.push({
63
+ label: 'Home',
64
+ path: normalized.slice(0, macIndex),
65
+ note: 'User home on macOS',
66
+ });
67
+ presets.push({
68
+ label: 'Root',
69
+ path: '/',
70
+ note: 'Entire filesystem on macOS',
71
+ });
72
+ return dedupePresets(presets);
73
+ }
74
+
75
+ const linuxIndex = normalized.indexOf('/.config/');
76
+ if (linuxIndex > 0) {
77
+ presets.push({
78
+ label: 'Home',
79
+ path: normalized.slice(0, linuxIndex),
80
+ note: 'User home on Linux',
81
+ });
82
+ presets.push({
83
+ label: 'Root',
84
+ path: '/',
85
+ note: 'Entire filesystem on Linux',
86
+ });
87
+ }
88
+
89
+ return dedupePresets(presets);
90
+ }
91
+
92
+ function dedupePresets(presets: SearchPreset[]): SearchPreset[] {
93
+ const seen = new Set<string>();
94
+ return presets.filter((preset) => {
95
+ const key = (preset.path ?? preset.roots?.join('|') ?? '').trim().toLowerCase();
96
+ if (!key || seen.has(key)) {
97
+ return false;
98
+ }
99
+ seen.add(key);
100
+ return true;
101
+ });
102
+ }
103
+
104
+ // ============================================================================
105
+ // App Component
106
+ // ============================================================================
107
+
108
+ function App() {
109
+ const [images, setImages] = createSignal<FileInfo[]>([]);
110
+ const [documents, setDocuments] = createSignal<FileInfo[]>([]);
111
+ const [appDataRoot, setAppDataRoot] = createSignal('');
112
+ const [appDataEntries, setAppDataEntries] = createSignal<AppDataEntry[]>([]);
113
+ const [settingsText, setSettingsText] = createSignal('');
114
+ const [status, setStatus] = createSignal('Loading app data...');
115
+
116
+ const [searchOpen, setSearchOpen] = createSignal(false);
117
+ const [availableRoots, setAvailableRoots] = createSignal<string[]>([]);
118
+ const [searchPresets, setSearchPresets] = createSignal<SearchPreset[]>([]);
119
+ const [lastKeyEvent, setLastKeyEvent] = createSignal('Waiting for keyboard input...');
120
+ const [pressedKeys, setPressedKeys] = createSignal<string[]>([]);
121
+ const [lastShortcut, setLastShortcut] = createSignal('No shortcut has fired yet');
122
+ const [browserLastKeyEvent, setBrowserLastKeyEvent] = createSignal('Waiting for browser keyboard input...');
123
+ const [browserPressedKeys, setBrowserPressedKeys] = createSignal<string[]>([]);
124
+ const [appInfo, setAppInfo] = createSignal<AppInfo | null>(null);
125
+ const [windowInfo, setWindowInfo] = createSignal<WindowInfo | null>(null);
126
+ const [lastWindowEvent, setLastWindowEvent] = createSignal('Waiting for window events...');
127
+ const [clipboardText, setClipboardText] = createSignal('');
128
+ const [monitorSummary, setMonitorSummary] = createSignal('Loading monitors...');
129
+ const [alwaysOnTop, setAlwaysOnTop] = createSignal(false);
130
+ const [clipboardToastVisible, setClipboardToastVisible] = createSignal(false);
131
+ const [clipboardToastTitle, setClipboardToastTitle] = createSignal('Clipboard text captured');
132
+ const [clipboardToastMessage, setClipboardToastMessage] = createSignal('');
133
+
134
+ let imageZoneRef: HTMLDivElement | undefined;
135
+ let documentZoneRef: HTMLDivElement | undefined;
136
+ let disposeInputKeyListener: (() => void) | undefined;
137
+ let disposeInputShortcutListener: (() => void) | undefined;
138
+ let disposeAppEventListener: (() => void) | undefined;
139
+ let disposeWindowEventListener: (() => void) | undefined;
140
+ let clipboardToastTimer: number | undefined;
141
+
142
+ onMount(() => {
143
+ const syncBrowserPressedKeys = (event: KeyboardEvent, pressed: boolean) => {
144
+ const label = event.key === ' ' ? 'Space' : event.key;
145
+ setBrowserLastKeyEvent(`${event.type} ${label} [code=${event.code}] [ctrl=${event.ctrlKey} shift=${event.shiftKey} alt=${event.altKey} meta=${event.metaKey}]`);
146
+ setBrowserPressedKeys((current) => {
147
+ const next = current.filter((entry) => entry !== label);
148
+ if (pressed) {
149
+ next.push(label);
150
+ }
151
+ return next;
152
+ });
153
+ };
154
+
155
+ const handleBrowserKeyDown = (event: KeyboardEvent) => {
156
+ syncBrowserPressedKeys(event, true);
157
+ };
158
+
159
+ const handleBrowserKeyUp = (event: KeyboardEvent) => {
160
+ syncBrowserPressedKeys(event, false);
161
+ };
162
+
163
+ const clearBrowserPressedKeys = () => {
164
+ setBrowserPressedKeys([]);
165
+ };
166
+
167
+ window.addEventListener('keydown', handleBrowserKeyDown);
168
+ window.addEventListener('keyup', handleBrowserKeyUp);
169
+ window.addEventListener('blur', clearBrowserPressedKeys);
170
+
171
+ if (imageZoneRef) {
172
+ const zone = fileDrop.createDropZone('images', imageZoneRef);
173
+ zone.onFiles((files: FileInfo[]) => {
174
+ console.log('Images dropped:', files);
175
+ setImages(files);
176
+ });
177
+ }
178
+
179
+ if (documentZoneRef) {
180
+ const zone = fileDrop.createDropZone('documents', documentZoneRef);
181
+ zone.onFiles((files: FileInfo[]) => {
182
+ console.log('Documents dropped:', files);
183
+ setDocuments(files);
184
+ });
185
+ }
186
+
187
+ void initializeAppData();
188
+
189
+ void initializeInputDemo();
190
+
191
+ void initializePlatformDemo();
192
+
193
+ onCleanup(() => {
194
+ if (clipboardToastTimer !== undefined) {
195
+ window.clearTimeout(clipboardToastTimer);
196
+ }
197
+ window.removeEventListener('keydown', handleBrowserKeyDown);
198
+ window.removeEventListener('keyup', handleBrowserKeyUp);
199
+ window.removeEventListener('blur', clearBrowserPressedKeys);
200
+ disposeInputKeyListener?.();
201
+ disposeInputShortcutListener?.();
202
+ disposeAppEventListener?.();
203
+ disposeWindowEventListener?.();
204
+ void input.unregisterShortcut('example.local.quick-help');
205
+ void input.unregisterShortcut('example.local.toggle-search');
206
+ void input.unregisterShortcut('example.local.close-search');
207
+ void input.unregisterShortcut('example.global.toggle-search');
208
+ });
209
+ });
210
+
211
+ async function initializePlatformDemo() {
212
+ try {
213
+ const [info, currentWindow, monitors] = await Promise.all([
214
+ appApi.getInfo(),
215
+ windowApi.getCurrent(),
216
+ appApi.getMonitors(),
217
+ ]);
218
+
219
+ setAppInfo(info);
220
+ setWindowInfo(currentWindow);
221
+ setAlwaysOnTop(false);
222
+ setMonitorSummary(formatMonitorSummary(monitors));
223
+ const initialClipboard = await appApi.getClipboardText();
224
+ setClipboardText(initialClipboard);
225
+
226
+ disposeAppEventListener = await appApi.onClipboardText((nextClipboard) => {
227
+ setClipboardText(nextClipboard);
228
+ showClipboardToast('Clipboard text captured', nextClipboard || 'Empty clipboard');
229
+ });
230
+
231
+ disposeWindowEventListener = await windowApi.onEvent((event: WindowEvent) => {
232
+ setLastWindowEvent(formatWindowEvent(event));
233
+ if (event.window) {
234
+ setWindowInfo(event.window);
235
+ }
236
+ });
237
+ } catch (error) {
238
+ const message = error instanceof Error ? error.message : String(error);
239
+ setStatus(`Platform API error: ${message}`);
240
+ }
241
+ }
242
+
243
+ function showClipboardToast(title: string, message: string) {
244
+ if (clipboardToastTimer !== undefined) {
245
+ window.clearTimeout(clipboardToastTimer);
246
+ }
247
+ setClipboardToastTitle(title);
248
+ setClipboardToastMessage(message || 'Empty clipboard');
249
+ setClipboardToastVisible(true);
250
+ clipboardToastTimer = window.setTimeout(() => {
251
+ setClipboardToastVisible(false);
252
+ }, 2600);
253
+ }
254
+
255
+ async function initializeInputDemo() {
256
+ try {
257
+ disposeInputKeyListener = await input.onKeyEvent((event: InputKeyEvent) => {
258
+ setLastKeyEvent(`${event.type} ${event.key} [pressed=${event.pressedKeys.join(' > ') || 'none'}]${event.ctrl ? ' +Ctrl' : ''}${event.shift ? ' +Shift' : ''}${event.alt ? ' +Alt' : ''}${event.meta ? ' +Meta' : ''}${event.global ? ' (global)' : ''}`);
259
+ setPressedKeys(event.pressedKeys);
260
+ });
261
+
262
+ disposeInputShortcutListener = input.onShortcut((shortcutEvent: ShortcutEvent) => {
263
+ setLastShortcut(`${shortcutEvent.id} -> ${shortcutEvent.event.key}`);
264
+
265
+ if (shortcutEvent.id === 'example.local.toggle-search') {
266
+ setSearchOpen((open) => !open);
267
+ setStatus('Local shortcut fired via native input API');
268
+ }
269
+
270
+ if (shortcutEvent.id === 'example.local.quick-help') {
271
+ setStatus('Single-key shortcut fired via native input API');
272
+ }
273
+
274
+ if (shortcutEvent.id === 'example.local.close-search') {
275
+ setSearchOpen(false);
276
+ }
277
+
278
+ if (shortcutEvent.id === 'example.global.toggle-search') {
279
+ setSearchOpen((open) => !open);
280
+ setStatus('Global hotkey fired via native input API');
281
+ }
282
+ });
283
+
284
+ await input.registerAppShortcut('example.local.quick-help', 'L');
285
+ await input.registerAppShortcut('example.local.toggle-search', 'Ctrl+Shift+L');
286
+ await input.registerAppShortcut('example.local.close-search', 'Escape');
287
+ await input.registerGlobalShortcut('example.global.toggle-search', 'Ctrl+Shift+Space');
288
+ setStatus('Input API ready');
289
+ } catch (error) {
290
+ const message = error instanceof Error ? error.message : String(error);
291
+ setStatus(`Input API error: ${message}`);
292
+ }
293
+ }
294
+
295
+ function formatWindowEvent(event: WindowEvent): string {
296
+ const windowLabel = event.window ? `${event.window.title} (${event.window.state})` : event.windowId;
297
+ return `${event.name} -> ${windowLabel}`;
298
+ }
299
+
300
+ function formatMonitorSummary(monitors: MonitorInfo[]): string {
301
+ if (!monitors.length) {
302
+ return 'No monitors reported';
303
+ }
304
+ const primary = monitors.find((monitor) => monitor.isPrimary) ?? monitors[0];
305
+ return `${monitors.length} monitor(s), primary ${primary.name} @ ${primary.scaleFactor.toFixed(2)}x`;
306
+ }
307
+
308
+ async function runWindowAction(label: string, action: () => Promise<WindowInfo | void>) {
309
+ try {
310
+ const result = await action();
311
+ if (result) {
312
+ setWindowInfo(result);
313
+ } else {
314
+ const current = await windowApi.getCurrent().catch(() => null);
315
+ if (current) {
316
+ setWindowInfo(current);
317
+ }
318
+ }
319
+ setStatus(label);
320
+ } catch (error) {
321
+ const message = error instanceof Error ? error.message : String(error);
322
+ setStatus(`${label} failed: ${message}`);
323
+ }
324
+ }
325
+
326
+ async function hideThenShowWindow() {
327
+ await runWindowAction('Window hidden', () => windowApi.hide());
328
+ window.setTimeout(() => {
329
+ void runWindowAction('Window shown again', () => windowApi.show());
330
+ }, 1000);
331
+ }
332
+
333
+ async function nudgeWindow() {
334
+ const current = windowInfo();
335
+ if (!current) {
336
+ return;
337
+ }
338
+ await runWindowAction('Window moved', () => windowApi.setPosition(current.position.x + 40, current.position.y + 40));
339
+ }
340
+
341
+ async function growWindow() {
342
+ const current = windowInfo();
343
+ if (!current) {
344
+ return;
345
+ }
346
+ await runWindowAction('Window resized', () => windowApi.setSize(current.size.width + 120, current.size.height + 80));
347
+ }
348
+
349
+ async function renameWindow() {
350
+ const nextTitle = `WindowPP Control Demo ${new Date().toLocaleTimeString()}`;
351
+ await runWindowAction('Window title updated', () => windowApi.setTitle(nextTitle));
352
+ }
353
+
354
+ async function toggleAlwaysOnTop() {
355
+ const nextValue = !alwaysOnTop();
356
+ await runWindowAction(nextValue ? 'Window pinned on top' : 'Window no longer on top', async () => {
357
+ const updated = await windowApi.setAlwaysOnTop(nextValue);
358
+ setAlwaysOnTop(nextValue);
359
+ return updated;
360
+ });
361
+ }
362
+
363
+ async function copyWindowTitle() {
364
+ const title = windowInfo()?.title ?? '';
365
+ try {
366
+ await appApi.setClipboardText(title);
367
+ setStatus('Window title copied to clipboard');
368
+ } catch (error) {
369
+ const message = error instanceof Error ? error.message : String(error);
370
+ setStatus(`Clipboard write failed: ${message}`);
371
+ }
372
+ }
373
+
374
+ async function refreshClipboard() {
375
+ try {
376
+ const nextClipboard = await appApi.getClipboardText();
377
+ setClipboardText(nextClipboard);
378
+ setStatus('Clipboard refreshed');
379
+ } catch (error) {
380
+ const message = error instanceof Error ? error.message : String(error);
381
+ setStatus(`Clipboard read failed: ${message}`);
382
+ }
383
+ }
384
+
385
+ async function copySampleClipboardText() {
386
+ const sample = `Example clipboard capture ${new Date().toLocaleTimeString()}`;
387
+ try {
388
+ await appApi.setClipboardText(sample);
389
+ setStatus('Sample clipboard text copied');
390
+ } catch (error) {
391
+ const message = error instanceof Error ? error.message : String(error);
392
+ setStatus(`Sample clipboard copy failed: ${message}`);
393
+ }
394
+ }
395
+
396
+ async function initializeAppData() {
397
+ try {
398
+ const root = await appData.getRoot();
399
+ const roots = await filesystem.listRoots();
400
+ const hasSettings = await appData.exists('settings.json');
401
+ const [settings, entries] = await Promise.all([
402
+ hasSettings ? appData.readText('settings.json') : Promise.resolve(''),
403
+ appData.readDir(''),
404
+ ]);
405
+ setAppDataRoot(root);
406
+ setAvailableRoots(roots);
407
+ setSearchPresets([
408
+ {
409
+ label: 'All Roots',
410
+ roots,
411
+ note: roots.length > 1 ? 'Search every mounted drive by default' : 'Search full filesystem by default',
412
+ },
413
+ ...inferSearchPresets(root),
414
+ ]);
415
+ setSettingsText(settings);
416
+ setAppDataEntries(entries);
417
+ setStatus('App data ready');
418
+ } catch (error) {
419
+ const message = error instanceof Error ? error.message : String(error);
420
+ setStatus(`App data error: ${message}`);
421
+ }
422
+ }
423
+
424
+ async function saveSettings() {
425
+ try {
426
+ await appData.writeText('settings.json', settingsText());
427
+ setAppDataEntries(await appData.readDir(''));
428
+ setStatus('settings.json saved');
429
+ } catch (error) {
430
+ const message = error instanceof Error ? error.message : String(error);
431
+ setStatus(`Save failed: ${message}`);
432
+ }
433
+ }
434
+
435
+ async function handleSearchResultClick(result: FileSearchResult) {
436
+ try {
437
+ if (result.kind === 'application') {
438
+ await filesystem.launchApplication(result.application.launchTarget);
439
+ setStatus(`Launched ${result.application.name}`);
440
+ } else {
441
+ await filesystem.openPath(result.path);
442
+ setStatus(`Opened ${result.path}`);
443
+ }
444
+ setSearchOpen(false);
445
+ } catch (error) {
446
+ const message = error instanceof Error ? error.message : String(error);
447
+ setStatus(`Open failed: ${message}`);
448
+ }
449
+ }
450
+
451
+ const appShortcutLabel = /Mac|iPhone|iPad|iPod/.test(navigator.platform) ? 'Cmd+Shift+L' : 'Ctrl+Shift+L';
452
+ const globalShortcutLabel = /Mac|iPhone|iPad|iPod/.test(navigator.platform) ? 'Cmd+Shift+Space' : 'Ctrl+Shift+Space';
453
+
454
+ return (
455
+ <div class="min-h-screen bg-[radial-gradient(circle_at_top,#f8fafc,#e2e8f0_40%,#cbd5e1)] px-6 py-8 text-slate-900">
456
+ <ClipboardToast
457
+ visible={clipboardToastVisible()}
458
+ title={clipboardToastTitle()}
459
+ message={clipboardToastMessage()}
460
+ />
461
+
462
+ {/* Floating Search Button */}
463
+ <FileSearchButton onClick={() => setSearchOpen(true)} />
464
+
465
+ {/* File Search Modal */}
466
+ <FileSearch
467
+ isOpen={searchOpen}
468
+ onClose={() => setSearchOpen(false)}
469
+ availableRoots={availableRoots}
470
+ searchPresets={searchPresets}
471
+ onResultClick={handleSearchResultClick}
472
+ />
473
+
474
+ {/* Main Content */}
475
+ <div class="mx-auto max-w-6xl">
476
+ {/* Header Card */}
477
+ <div class="mb-8 rounded-4xl border border-white/70 bg-white/80 p-8 shadow-2xl shadow-slate-400/20 backdrop-blur">
478
+ <div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
479
+ <div>
480
+ <p class="mb-2 text-sm font-semibold uppercase tracking-[0.3em] text-slate-500">WindowPP Playground</p>
481
+ <h1 class="text-4xl font-bold tracking-tight text-slate-900">File drop, app data, and native file search in one example app.</h1>
482
+ <p class="mt-3 max-w-3xl text-sm leading-6 text-slate-600">
483
+ The floating command palette now drives real native filesystem search bridge. Use it to test recursive search behavior on Windows, macOS, and Linux without changing framework code for each platform.
484
+ </p>
485
+ </div>
486
+ <div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
487
+ <div class="font-semibold text-slate-800">Status</div>
488
+ <div>{status()}</div>
489
+ </div>
490
+ <div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
491
+ <div class="font-semibold text-slate-800">Input API</div>
492
+ <div>App: {appShortcutLabel}</div>
493
+ <div>Global: {globalShortcutLabel}</div>
494
+ </div>
495
+ <div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
496
+ <div class="font-semibold text-slate-800">Platform API</div>
497
+ <div>{appInfo()?.name ?? 'Loading app info...'}</div>
498
+ <div>{monitorSummary()}</div>
499
+ </div>
500
+ <A
501
+ href="/settings"
502
+ class="flex items-center gap-1.5 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600 hover:bg-slate-100 transition-colors select-none"
503
+ >
504
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 text-slate-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
505
+ <path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
506
+ <circle cx="12" cy="12" r="3"/>
507
+ </svg>
508
+ <span class="font-semibold text-slate-800">Settings</span>
509
+ </A>
510
+ </div>
511
+ </div>
512
+
513
+ <div class="mb-8 grid gap-4 lg:grid-cols-2 xl:grid-cols-4">
514
+ <div class="rounded-3xl border border-white/70 bg-white/85 p-5 shadow-xl shadow-slate-400/10 backdrop-blur">
515
+ <div class="text-sm font-semibold uppercase tracking-[0.2em] text-slate-500">App Key Event</div>
516
+ <div class="mt-3 text-sm text-slate-700">{lastKeyEvent()}</div>
517
+ </div>
518
+ <div class="rounded-3xl border border-white/70 bg-white/85 p-5 shadow-xl shadow-slate-400/10 backdrop-blur">
519
+ <div class="text-sm font-semibold uppercase tracking-[0.2em] text-slate-500">App Pressed Keys</div>
520
+ <div class="mt-3 text-sm text-slate-700">{pressedKeys().length ? pressedKeys().join(' > ') : 'None'}</div>
521
+ </div>
522
+ <div class="rounded-3xl border border-white/70 bg-white/85 p-5 shadow-xl shadow-slate-400/10 backdrop-blur">
523
+ <div class="text-sm font-semibold uppercase tracking-[0.2em] text-slate-500">Browser App Keys</div>
524
+ <div class="mt-3 text-sm text-slate-700">{browserPressedKeys().length ? browserPressedKeys().join(' > ') : 'None'}</div>
525
+ </div>
526
+ <div class="rounded-3xl border border-white/70 bg-white/85 p-5 shadow-xl shadow-slate-400/10 backdrop-blur">
527
+ <div class="text-sm font-semibold uppercase tracking-[0.2em] text-slate-500">Browser Last Event</div>
528
+ <div class="mt-3 text-sm text-slate-700">{browserLastKeyEvent()}</div>
529
+ </div>
530
+ <div class="rounded-3xl border border-white/70 bg-white/85 p-5 shadow-xl shadow-slate-400/10 backdrop-blur xl:col-span-4">
531
+ <div class="text-sm font-semibold uppercase tracking-[0.2em] text-slate-500">Last Shortcut</div>
532
+ <div class="mt-3 text-sm text-slate-700">{lastShortcut()}</div>
533
+ </div>
534
+ </div>
535
+
536
+ <div class="mb-8 rounded-[28px] border border-white/70 bg-white/85 p-6 shadow-xl shadow-slate-400/15 backdrop-blur">
537
+ <div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
538
+ <div>
539
+ <h2 class="text-xl font-semibold text-slate-800">Platform Control API</h2>
540
+ <p class="mt-2 max-w-3xl text-sm text-slate-500">
541
+ Cross-platform app and window control from the frontend. App scope is inside this WindowPP app. Global scope is OS-wide for supported shortcut backends.
542
+ </p>
543
+ </div>
544
+ <div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
545
+ <div class="font-semibold text-slate-800">Last Window Event</div>
546
+ <div>{lastWindowEvent()}</div>
547
+ </div>
548
+ </div>
549
+
550
+ <div class="mt-6 grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
551
+ <div class="rounded-3xl border border-slate-200 bg-slate-50 p-5 text-sm text-slate-700">
552
+ <div class="text-sm font-semibold uppercase tracking-[0.2em] text-slate-500">Current Window</div>
553
+ <div class="mt-3 space-y-2">
554
+ <div>ID: {windowInfo()?.id ?? 'Loading...'}</div>
555
+ <div>Title: {windowInfo()?.title ?? 'Loading...'}</div>
556
+ <div>State: {windowInfo()?.state ?? 'Loading...'}</div>
557
+ <div>Visible: {windowInfo() ? (windowInfo()!.visible ? 'true' : 'false') : 'Loading...'}</div>
558
+ <div>Focused: {windowInfo() ? (windowInfo()!.focused ? 'true' : 'false') : 'Loading...'}</div>
559
+ <div>
560
+ Position: {windowInfo() ? `${windowInfo()!.position.x}, ${windowInfo()!.position.y}` : 'Loading...'}
561
+ </div>
562
+ <div>
563
+ Size: {windowInfo() ? `${windowInfo()!.size.width} x ${windowInfo()!.size.height}` : 'Loading...'}
564
+ </div>
565
+ <div>Scale: {windowInfo() ? windowInfo()!.scaleFactor.toFixed(2) : 'Loading...'}</div>
566
+ </div>
567
+ </div>
568
+
569
+ <div class="rounded-3xl border border-slate-200 bg-slate-50 p-5 text-sm text-slate-700">
570
+ <div class="text-sm font-semibold uppercase tracking-[0.2em] text-slate-500">Application</div>
571
+ <div class="mt-3 space-y-2">
572
+ <div>Name: {appInfo()?.name ?? 'Loading...'}</div>
573
+ <div>Version: {appInfo()?.version ?? 'Loading...'}</div>
574
+ <div>Identifier: {appInfo()?.identifier ?? 'Loading...'}</div>
575
+ <div>Clipboard: {clipboardText() || 'Empty'}</div>
576
+ </div>
577
+ </div>
578
+ </div>
579
+
580
+ <div class="mt-6 flex flex-wrap gap-3">
581
+ <button class="rounded-full bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700" onClick={() => void runWindowAction('Window info refreshed', () => windowApi.getCurrent())}>Refresh</button>
582
+ <button class="rounded-full bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700" onClick={() => void renameWindow()}>Rename Window</button>
583
+ <button class="rounded-full bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700" onClick={() => void runWindowAction('Window focused', () => windowApi.focus())}>Focus</button>
584
+ <button class="rounded-full bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700" onClick={() => void runWindowAction('Window minimized', () => windowApi.minimize())}>Minimize</button>
585
+ <button class="rounded-full bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700" onClick={() => void runWindowAction('Window maximized', () => windowApi.maximize())}>Maximize</button>
586
+ <button class="rounded-full bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700" onClick={() => void runWindowAction('Window restored', () => windowApi.restore())}>Restore</button>
587
+ <button class="rounded-full bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700" onClick={() => void runWindowAction('Window centered', () => windowApi.center())}>Center</button>
588
+ <button class="rounded-full bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700" onClick={() => void nudgeWindow()}>Move +40,+40</button>
589
+ <button class="rounded-full bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700" onClick={() => void growWindow()}>Resize +120,+80</button>
590
+ <button class="rounded-full bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700" onClick={() => void toggleAlwaysOnTop()}>{alwaysOnTop() ? 'Disable Always On Top' : 'Enable Always On Top'}</button>
591
+ <button class="rounded-full bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700" onClick={() => void hideThenShowWindow()}>Hide 1s</button>
592
+ <button class="rounded-full bg-slate-200 px-4 py-2 text-sm font-medium text-slate-800 hover:bg-slate-300" onClick={() => void copyWindowTitle()}>Copy Title</button>
593
+ <button class="rounded-full bg-emerald-500 px-4 py-2 text-sm font-medium text-white hover:bg-emerald-600" onClick={() => void copySampleClipboardText()}>Copy Sample Clipboard Text</button>
594
+ <button class="rounded-full bg-slate-200 px-4 py-2 text-sm font-medium text-slate-800 hover:bg-slate-300" onClick={() => void refreshClipboard()}>Read Clipboard</button>
595
+ </div>
596
+ </div>
597
+
598
+ {/* Main Grid */}
599
+ <div class="mb-6 grid gap-6 xl:grid-cols-[1.4fr_0.9fr]">
600
+ {/* App Data Section */}
601
+ <div class="rounded-[28px] border border-white/70 bg-white/85 p-6 shadow-xl shadow-slate-400/15 backdrop-blur">
602
+ <div class="mb-4 flex items-start justify-between gap-4">
603
+ <div>
604
+ <h2 class="text-xl font-semibold text-slate-800">App Data</h2>
605
+ <p class="mt-1 text-sm text-slate-500 break-all">{appDataRoot()}</p>
606
+ </div>
607
+ <button
608
+ class="rounded-full border border-slate-300 bg-slate-100 px-3 py-1.5 text-sm font-medium text-slate-700 hover:border-slate-400 hover:bg-slate-200"
609
+ onClick={() => setSearchOpen(true)}
610
+ >
611
+ Open Search Palette
612
+ </button>
613
+ </div>
614
+ <p class="mb-4 text-sm text-slate-500">
615
+ Startup creates a per-user app-data folder automatically. Files inside it are optional and only appear when your app writes them.
616
+ </p>
617
+ <div class="grid gap-6 lg:grid-cols-2 lg:items-start">
618
+ <div>
619
+ <label for="settings-json" class="mb-2 block text-sm font-medium text-slate-700">settings.json</label>
620
+ <textarea
621
+ id="settings-json"
622
+ class="min-h-48 w-full rounded-2xl border border-slate-200 bg-slate-50 p-3 font-mono text-sm text-slate-800 outline-none ring-0 placeholder:text-slate-400 focus:border-slate-400"
623
+ title="settings.json editor"
624
+ placeholder="Optional: write settings.json here and click Save Settings"
625
+ value={settingsText()}
626
+ onInput={(event) => setSettingsText(event.currentTarget.value)}
627
+ />
628
+ <button
629
+ class="mt-3 rounded-full bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700"
630
+ onClick={() => void saveSettings()}
631
+ >
632
+ Save Settings
633
+ </button>
634
+ <p class="mt-3 text-xs text-slate-500">
635
+ App input demo: use {appShortcutLabel} in the focused app window or {globalShortcutLabel} from anywhere.
636
+ </p>
637
+ </div>
638
+ <div>
639
+ <h3 class="mb-2 text-sm font-medium text-slate-700">Current app-data entries</h3>
640
+ <div class="space-y-2">
641
+ <Show
642
+ when={appDataEntries().length > 0}
643
+ fallback={
644
+ <div class="rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-sm text-slate-500">
645
+ Folder is empty. Save settings or write files from the frontend/backend to populate it.
646
+ </div>
647
+ }
648
+ >
649
+ <For each={appDataEntries()}>
650
+ {(entry) => (
651
+ <div class="rounded-2xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm">
652
+ <div class="font-medium text-slate-800">{entry.name}</div>
653
+ <div class="break-all text-slate-500">{entry.path}</div>
654
+ </div>
655
+ )}
656
+ </For>
657
+ </Show>
658
+ </div>
659
+ </div>
660
+ </div>
661
+ </div>
662
+
663
+ {/* Presets Panel */}
664
+ <PresetsPanel
665
+ presets={searchPresets}
666
+ onPresetClick={() => setSearchOpen(true)}
667
+ />
668
+ </div>
669
+
670
+ {/* Drop Zones */}
671
+ <div class="grid gap-6 lg:grid-cols-2">
672
+ <div class="rounded-[28px] border border-white/70 bg-white/85 p-6 shadow-xl shadow-slate-400/15 backdrop-blur">
673
+ <h2 class="mb-4 flex items-center gap-2 text-xl font-semibold text-slate-700">
674
+ <span>Images</span>
675
+ <span class="text-sm text-slate-500">(jpg, png, gif)</span>
676
+ </h2>
677
+ <div
678
+ ref={imageZoneRef}
679
+ class="wpp-dropzone"
680
+ data-dropzone="images"
681
+ >
682
+ <div class="wpp-dropzone-content">
683
+ <svg class="wpp-dropzone-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
684
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
685
+ </svg>
686
+ <span class="wpp-dropzone-text">Drop images here</span>
687
+ <span class="wpp-dropzone-hint">jpg, png, gif</span>
688
+ </div>
689
+ </div>
690
+
691
+ <Show when={images().length > 0}>
692
+ <div class="wpp-dropzone-files mt-4">
693
+ <For each={images()}>
694
+ {(f: FileInfo) => (
695
+ <div class="wpp-dropzone-file-item">
696
+ <div class="wpp-dropzone-file-icon">
697
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" class="w-5 h-5">
698
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
699
+ </svg>
700
+ </div>
701
+ <div class="wpp-dropzone-file-info">
702
+ <span class="wpp-dropzone-file-name">{f.name}</span>
703
+ <span class="wpp-dropzone-file-meta">{formatFileSize(f.size)}</span>
704
+ </div>
705
+ </div>
706
+ )}
707
+ </For>
708
+ <button
709
+ class="mt-2 w-full rounded-full bg-slate-200 px-3 py-1 text-sm hover:bg-slate-300"
710
+ onClick={() => setImages([])}
711
+ >
712
+ Clear Images
713
+ </button>
714
+ </div>
715
+ </Show>
716
+ </div>
717
+
718
+ <div class="rounded-[28px] border border-white/70 bg-white/85 p-6 shadow-xl shadow-slate-400/15 backdrop-blur">
719
+ <h2 class="mb-4 flex items-center gap-2 text-xl font-semibold text-slate-700">
720
+ <span>Documents</span>
721
+ <span class="text-sm text-slate-500">(pdf, doc, txt)</span>
722
+ </h2>
723
+ <div
724
+ ref={documentZoneRef}
725
+ class="wpp-dropzone"
726
+ data-dropzone="documents"
727
+ >
728
+ <div class="wpp-dropzone-content">
729
+ <svg class="wpp-dropzone-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
730
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
731
+ </svg>
732
+ <span class="wpp-dropzone-text">Drop documents here</span>
733
+ <span class="wpp-dropzone-hint">pdf, doc, txt</span>
734
+ </div>
735
+ </div>
736
+
737
+ <Show when={documents().length > 0}>
738
+ <div class="wpp-dropzone-files mt-4">
739
+ <For each={documents()}>
740
+ {(f: FileInfo) => (
741
+ <div class="wpp-dropzone-file-item">
742
+ <div class="wpp-dropzone-file-icon">
743
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" class="w-5 h-5">
744
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
745
+ </svg>
746
+ </div>
747
+ <div class="wpp-dropzone-file-info">
748
+ <span class="wpp-dropzone-file-name">{f.name}</span>
749
+ <span class="wpp-dropzone-file-meta">{formatFileSize(f.size)}</span>
750
+ </div>
751
+ </div>
752
+ )}
753
+ </For>
754
+ <button
755
+ class="mt-2 w-full rounded-full bg-slate-200 px-3 py-1 text-sm hover:bg-slate-300"
756
+ onClick={() => setDocuments([])}
757
+ >
758
+ Clear Documents
759
+ </button>
760
+ </div>
761
+ </Show>
762
+ </div>
763
+ </div>
764
+
765
+ {/* How It Works */}
766
+ <div class="mt-8 rounded-[28px] border border-white/70 bg-white/85 p-6 shadow-xl shadow-slate-400/15 backdrop-blur">
767
+ <h2 class="mb-4 text-xl font-semibold text-slate-700">How It Works</h2>
768
+ <ul class="space-y-2 text-slate-600">
769
+ <li>1. Drag files over the window - cursor shows "none" outside zones</li>
770
+ <li>2. Hover over a drop zone - cursor changes to "copy" and zone highlights</li>
771
+ <li>3. Drop files on a zone - files are processed and displayed</li>
772
+ <li>4. Open search palette with {appShortcutLabel} in the focused app or {globalShortcutLabel} system-wide to live-test fs.searchFiles.</li>
773
+ <li>5. Change root path to app data, home, or a drive/root path to compare platform behavior.</li>
774
+ </ul>
775
+ </div>
776
+ </div>
777
+ </div>
778
+ );
779
+ }
780
+
781
+ export default App;