experiments in a post-browser web
1# Peek API Reference
2
3The Peek API (`window.app`) is the unified interface for all `peek://` pages to interact with the system. It provides window management, data storage, messaging, shortcuts, theming, and more.
4
5This API is implemented by both the Electron and Tauri backends, ensuring frontend code works unchanged across backends.
6
7## Table of Contents
8
9- [Context Detection](#context-detection)
10- [Window Management](#window-management)
11- [Datastore](#datastore)
12- [PubSub Messaging](#pubsub-messaging)
13- [Keyboard Shortcuts](#keyboard-shortcuts)
14- [Commands (Command Palette)](#commands-command-palette)
15- [Theme](#theme)
16- [Escape Handling](#escape-handling)
17- [IZUI State](#izui-state)
18- [Logging](#logging)
19- [Debug Mode](#debug-mode)
20- [App Control](#app-control)
21- [Extensions API](#extensions-api)
22- [Settings API](#settings-api)
23- [Response Format](#response-format)
24
25---
26
27## Context Detection
28
29```javascript
30// Check if running in peek:// context
31if (window.app) {
32 // API available
33}
34
35// Source address of current page
36const source = window.location.toString();
37
38// Context types:
39// - peek://app/... Core application pages
40// - peek://ext/{id}/... Extension pages
41```
42
43---
44
45## Window Management
46
47### `window.app.window.open(url, options)`
48
49Open a new window.
50
51```javascript
52const result = await window.app.window.open('peek://app/settings/settings.html', {
53 key: 'settings', // Reuse window with same key
54 width: 900, // Window width
55 height: 650, // Window height
56 x: 100, // X position
57 y: 100, // Y position
58 title: 'Settings', // Window title
59 modal: true, // Modal behavior
60 transparent: false, // Transparent background
61 decorations: true, // Window decorations
62 alwaysOnTop: false, // Stay on top
63 visible: true, // Initially visible
64 resizable: true, // Allow resize
65 draggable: true, // Allow click-and-hold drag (default: true)
66 keepLive: false, // Keep window alive when closed
67 escapeMode: 'close' // ESC key behavior: 'close', 'navigate', or 'auto'
68});
69// Returns: { success: true, id: 'window_label' }
70```
71
72### `window.app.window.close(id?)`
73
74Close a window. If no id, closes current window.
75
76```javascript
77await window.app.window.close(); // Close current
78await window.app.window.close('settings'); // Close by id
79await window.app.window.close({ id: 'settings' }); // Object form
80```
81
82### `window.app.window.hide(id?)` / `window.app.window.show(id?)`
83
84Toggle window visibility.
85
86```javascript
87await window.app.window.hide('main');
88await window.app.window.show('main');
89```
90
91### `window.app.window.focus(id?)`
92
93Bring window to front and focus it.
94
95```javascript
96await window.app.window.focus('settings');
97```
98
99### `window.app.window.list()`
100
101List all open windows.
102
103```javascript
104const result = await window.app.window.list();
105// Returns: {
106// success: true,
107// data: [{
108// id: 'main',
109// label: 'main',
110// url: 'peek://app/background.html',
111// source: 'peek://app/background.html',
112// visible: false,
113// focused: false
114// }, ...]
115// }
116```
117
118### `window.app.window.exists(id)`
119
120Check if a window exists.
121
122```javascript
123const result = await window.app.window.exists('settings');
124// Returns: { success: true, data: true }
125```
126
127### `window.app.invoke('window-animate', options)`
128
129Animate a window's position and/or size.
130
131```javascript
132// Animate to new position
133await window.app.invoke('window-animate', {
134 id: windowId, // Window ID (optional, defaults to current)
135 to: { x: 100, y: 100 }, // Target bounds (required)
136 duration: 150 // Animation duration in ms (default: 150)
137});
138
139// Animate from specific position
140await window.app.invoke('window-animate', {
141 id: windowId,
142 from: { x: 0, y: -600 }, // Starting bounds (optional, defaults to current)
143 to: { x: 0, y: 0, width: 800, height: 600 },
144 duration: 200
145});
146// Uses easeOutQuad easing for smooth deceleration
147```
148
149### `window.app.invoke('window-set-always-on-top', options)`
150
151Pin a window to stay on top of other windows.
152
153```javascript
154// Pin with normal level
155await window.app.invoke('window-set-always-on-top', {
156 id: windowId,
157 value: true
158});
159
160// Pin above other app windows (macOS)
161await window.app.invoke('window-set-always-on-top', {
162 id: windowId,
163 value: true,
164 level: 'floating'
165});
166
167// Pin above all windows (macOS)
168await window.app.invoke('window-set-always-on-top', {
169 id: windowId,
170 value: true,
171 level: 'screen-saver'
172});
173
174// Unpin
175await window.app.invoke('window-set-always-on-top', {
176 id: windowId,
177 value: false
178});
179```
180
181---
182
183## Datastore
184
185All datastore methods return `{ success: boolean, data?: any, error?: string }`.
186
187### Addresses
188
189```javascript
190// Add a new address
191const result = await window.app.datastore.addAddress('https://example.com', {
192 title: 'Example',
193 favicon: 'https://example.com/favicon.ico'
194});
195// Returns: { success: true, data: { id: 'addr_123', ... } }
196
197// Get address by ID
198const addr = await window.app.datastore.getAddress('addr_123');
199
200// Update address
201await window.app.datastore.updateAddress('addr_123', {
202 title: 'New Title'
203});
204
205// Query addresses
206const results = await window.app.datastore.queryAddresses({
207 uri: 'example.com', // Partial match
208 limit: 10,
209 offset: 0
210});
211```
212
213### Visits
214
215```javascript
216// Add a visit
217await window.app.datastore.addVisit('addr_123', {
218 referrer: 'addr_456'
219});
220
221// Query visits
222const visits = await window.app.datastore.queryVisits({
223 addressId: 'addr_123',
224 limit: 50
225});
226```
227
228### Tags
229
230```javascript
231// Get or create a tag
232const tag = await window.app.datastore.getOrCreateTag('important');
233// Returns: { success: true, data: { id: 'tag_123', name: 'important' } }
234
235// Tag an address
236await window.app.datastore.tagAddress('addr_123', 'tag_123');
237
238// Untag an address
239await window.app.datastore.untagAddress('addr_123', 'tag_123');
240
241// Get tags for an address
242const tags = await window.app.datastore.getAddressTags('addr_123');
243```
244
245### Generic Table Access
246
247```javascript
248// Get all rows from a table
249const table = await window.app.datastore.getTable('extensions');
250// Returns: { success: true, data: { row_id: { ... }, ... } }
251
252// Set a row
253await window.app.datastore.setRow('extensions', 'my-ext', {
254 name: 'My Extension',
255 enabled: true
256});
257```
258
259### Statistics
260
261```javascript
262const stats = await window.app.datastore.getStats();
263// Returns: { success: true, data: {
264// addresses: 150,
265// visits: 1200,
266// tags: 25
267// }}
268```
269
270---
271
272## PubSub Messaging
273
274Cross-window communication via publish/subscribe.
275
276### Scopes
277
278```javascript
279window.app.scopes = {
280 SYSTEM: 1, // System messages
281 SELF: 2, // Same source only
282 GLOBAL: 3 // All windows
283};
284```
285
286### `window.app.publish(topic, message, scope)`
287
288Publish a message.
289
290```javascript
291window.app.publish('settings:changed', { theme: 'dark' }, window.app.scopes.GLOBAL);
292```
293
294### `window.app.subscribe(topic, callback, scope)`
295
296Subscribe to messages.
297
298```javascript
299window.app.subscribe('settings:changed', (msg) => {
300 console.log('Settings changed:', msg.data);
301}, window.app.scopes.GLOBAL);
302```
303
304---
305
306## Keyboard Shortcuts
307
308### `window.app.shortcuts.register(shortcut, callback, options)`
309
310Register a keyboard shortcut.
311
312```javascript
313// Local shortcut (only when app focused)
314window.app.shortcuts.register('Command+K', () => {
315 console.log('Command+K pressed');
316});
317
318// Global shortcut (works even when app not focused)
319window.app.shortcuts.register('Option+Space', () => {
320 console.log('Global shortcut triggered');
321}, { global: true });
322```
323
324**Shortcut format:**
325- Modifiers: `Command`, `Control`, `Alt`, `Option`, `Shift`, `CommandOrControl`
326- Keys: `A-Z`, `0-9`, `F1-F12`, `Space`, `Enter`, `Escape`, `ArrowUp`, etc.
327- Examples: `Command+Shift+P`, `Alt+1`, `Option+ArrowDown`
328
329### `window.app.shortcuts.unregister(shortcut, options)`
330
331Unregister a shortcut.
332
333```javascript
334window.app.shortcuts.unregister('Command+K');
335window.app.shortcuts.unregister('Option+Space', { global: true });
336```
337
338---
339
340## Commands (Command Palette)
341
342Register commands that appear in the command palette.
343
344### `window.app.commands.register(command)`
345
346```javascript
347window.app.commands.register({
348 name: 'my-extension:do-thing',
349 description: 'Do the thing',
350 execute: () => {
351 console.log('Doing the thing');
352 }
353});
354```
355
356### `window.app.commands.unregister(name)`
357
358```javascript
359window.app.commands.unregister('my-extension:do-thing');
360```
361
362### `window.app.commands.getAll()`
363
364```javascript
365const commands = await window.app.commands.getAll();
366```
367
368---
369
370## Theme
371
372Manage application themes and color schemes.
373
374### `window.app.theme.get()`
375
376Get current theme state.
377
378```javascript
379const state = await window.app.theme.get();
380// Returns: {
381// success: true,
382// data: {
383// themeId: 'peek', // Active theme ID
384// colorScheme: 'system', // User preference: 'system', 'light', 'dark'
385// isDark: true, // Whether dark mode is active
386// effectiveScheme: 'dark' // Resolved scheme after system preference
387// }
388// }
389```
390
391### `window.app.theme.setTheme(themeId)`
392
393Set the active theme.
394
395```javascript
396await window.app.theme.setTheme('peek');
397// Broadcasts 'theme:themeChanged' event to all windows
398```
399
400### `window.app.theme.setColorScheme(scheme)`
401
402Set color scheme preference.
403
404```javascript
405await window.app.theme.setColorScheme('dark'); // Force dark mode
406await window.app.theme.setColorScheme('light'); // Force light mode
407await window.app.theme.setColorScheme('system'); // Follow OS preference
408// Broadcasts 'theme:changed' event to all windows
409```
410
411### `window.app.theme.list()`
412
413List available themes.
414
415```javascript
416const result = await window.app.theme.list();
417// Returns: {
418// success: true,
419// data: [
420// { id: 'basic', name: 'Basic', version: '1.0.0' },
421// { id: 'peek', name: 'Peek', version: '1.0.0' }
422// ]
423// }
424```
425
426### Theme Events
427
428Listen for theme changes:
429
430```javascript
431// Theme changed (different theme selected)
432window.app.subscribe('theme:themeChanged', (msg) => {
433 console.log('Theme changed to:', msg.themeId);
434}, window.app.scopes.GLOBAL);
435
436// Color scheme changed
437window.app.subscribe('theme:changed', (msg) => {
438 console.log('Color scheme changed to:', msg.colorScheme);
439}, window.app.scopes.GLOBAL);
440```
441
442---
443
444## Escape Handling
445
446Handle the ESC key to prevent window from closing.
447
448```javascript
449window.app.escape.onEscape(() => {
450 if (hasUnsavedChanges) {
451 showConfirmDialog();
452 return { handled: true }; // Prevent close
453 }
454 return { handled: false }; // Allow close
455});
456```
457
458---
459
460## IZUI State
461
462Query the IZUI state machine to determine if the current session is transient (invoked from another app) or active (invoked within Peek). See `docs/izui.md` for full details.
463
464### `window.app.izui.isTransient()`
465
466Check if the current session was invoked transiently (app wasn't focused).
467
468```javascript
469const isTransient = await window.app.izui.isTransient();
470if (isTransient) {
471 // User invoked from another app - show simplified UI
472 currentMode = 'default';
473} else {
474 // User is working within Peek - show contextual UI
475 currentMode = targetWindow.mode;
476}
477```
478
479### `window.app.izui.getEffectiveMode()`
480
481Get the effective mode for display. Returns `'default'` for transient sessions, `'active'` otherwise.
482
483```javascript
484const mode = await window.app.izui.getEffectiveMode();
485```
486
487### `window.app.izui.getState()`
488
489Get the current IZUI state.
490
491```javascript
492const state = await window.app.izui.getState();
493// Returns: 'idle' | 'transient' | 'active' | 'overlay'
494```
495
496---
497
498## Logging
499
500Log messages to the backend console.
501
502```javascript
503window.app.log('Something happened', { detail: 'value' });
504// Output in terminal: [peek://app/mypage.html] Something happened { detail: 'value' }
505```
506
507---
508
509## Debug Mode
510
511```javascript
512if (window.app.debug) {
513 console.log('Debug mode enabled');
514}
515
516// Debug levels
517window.app.debugLevels = { BASIC: 1, FIRST_RUN: 2 };
518window.app.debugLevel; // Current level
519```
520
521---
522
523## App Control
524
525Control application lifecycle.
526
527### `window.app.quit()`
528
529Quit the application.
530
531```javascript
532window.app.quit();
533```
534
535### `window.app.restart()`
536
537Restart the application (relaunch and quit).
538
539```javascript
540window.app.restart();
541```
542
543---
544
545## Extensions API
546
547Only available in core app pages (`peek://app/...`).
548
549### Hybrid Extension Architecture
550
551Peek uses a hybrid extension loading model:
552
553- **Built-in extensions** (`cmd`, `groups`, `peeks`, `slides`) run as iframes in a single extension host window for memory efficiency
554- **External extensions** (user-installed) run in separate BrowserWindows for crash isolation
555
556Both types are accessible via the same API - the loading mode is transparent to callers.
557
558### URL Schemes
559
560- Built-in consolidated: `peek://cmd/background.html`, `peek://groups/background.html`, etc.
561- External: `peek://ext/{id}/background.html`
562
563Each extension has a unique origin for isolation regardless of loading mode.
564
565### API Methods
566
567```javascript
568// Check permission
569if (window.app.extensions._hasPermission()) {
570 // List running extensions (includes both consolidated and separate window extensions)
571 const exts = await window.app.extensions.list();
572 // Returns: { success: true, data: [{ id, manifest, status }, ...] }
573
574 // Load/unload extensions
575 await window.app.extensions.load('my-extension');
576 await window.app.extensions.unload('my-extension');
577 await window.app.extensions.reload('my-extension');
578
579 // Extension management
580 const all = await window.app.extensions.getAll();
581 const ext = await window.app.extensions.get('my-extension');
582 await window.app.extensions.add(folderPath, manifest, enabled);
583 await window.app.extensions.remove('my-extension');
584 await window.app.extensions.update('my-extension', { enabled: false });
585}
586```
587
588---
589
590## Settings API
591
592Only available in extension pages (`peek://ext/{id}/...`).
593
594```javascript
595// Get all settings
596const settings = await window.app.settings.get();
597
598// Set all settings
599await window.app.settings.set({ theme: 'dark' });
600
601// Get/set individual keys
602const theme = await window.app.settings.getKey('theme');
603await window.app.settings.setKey('theme', 'light');
604```
605
606---
607
608## Response Format
609
610All async API methods return a consistent response format:
611
612```javascript
613{
614 success: true, // Operation succeeded
615 data: any, // Result data (if applicable)
616 error: string // Error message (if success: false)
617}
618```
619
620Always check `success` before using `data`:
621
622```javascript
623const result = await window.app.datastore.getAddress(id);
624if (result.success) {
625 console.log(result.data);
626} else {
627 console.error(result.error);
628}
629```
630
631---
632
633## Implementation Notes
634
635The Peek API is implemented in:
636- **Electron**: `preload.js` using Electron's `contextBridge`
637- **Tauri**: `backend/tauri/preload.js` injected via `initialization_script`
638
639Both implementations provide identical API surfaces, allowing frontend code to work unchanged across backends.