experiments in a post-browser web
at main 639 lines 15 kB view raw view rendered
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.