experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

add explicit escape navigational handling, active vs transient mode, and apis

+300 -30
+54
DEVELOPMENT.md
··· 65 65 66 66 Profile data stored in `{userData}/{PROFILE}/` directory. 67 67 68 + ## Window API 69 + 70 + ### Opening Windows 71 + 72 + ```javascript 73 + import windows from './windows.js'; 74 + 75 + // Modal window (closes on blur/ESC) 76 + windows.openModalWindow(url, options); 77 + 78 + // Regular window 79 + windows.createWindow(url, options); 80 + ``` 81 + 82 + ### Window Options 83 + 84 + | Option | Type | Description | 85 + |--------|------|-------------| 86 + | `width`, `height` | number | Window dimensions | 87 + | `x`, `y` | number | Window position | 88 + | `modal` | boolean | Frameless, closes on blur | 89 + | `key` | string | Unique ID for window reuse | 90 + | `keepLive` | boolean | Hide instead of close | 91 + | `escapeMode` | string | ESC behavior: `'close'`, `'navigate'`, `'auto'` | 92 + | `trackingSource` | string | Source for visit tracking | 93 + 94 + ### Escape Handling 95 + 96 + Windows can control how ESC behaves using `escapeMode`: 97 + 98 + - **`'close'`** (default): ESC immediately closes/hides the window 99 + - **`'navigate'`**: Renderer handles ESC first; closes only when renderer signals root state 100 + - **`'auto'`**: Transient windows (opened via hotkey from another app) close immediately; active windows use navigate behavior 101 + 102 + #### Using escapeMode: 'navigate' 103 + 104 + 1. Open window with `escapeMode: 'navigate'`: 105 + ```javascript 106 + windows.createWindow(url, { escapeMode: 'navigate' }); 107 + ``` 108 + 109 + 2. Register escape handler in renderer: 110 + ```javascript 111 + api.escape.onEscape(() => { 112 + if (canNavigateBack) { 113 + navigateBack(); 114 + return { handled: true }; // ESC was handled internally 115 + } 116 + return { handled: false }; // At root, let window close 117 + }); 118 + ``` 119 + 120 + See `notes/escape-navigation.md` for full design details. 121 + 68 122 ## App Icon Generation 69 123 70 124 The macOS app icon is generated from a source PNG using ImageMagick. The process applies rounded corners and adds padding to match macOS icon guidelines.
+13 -5
TODO.md
··· 30 30 31 31 - [ ] App cmds (eg Editor -> cmd to edit note X) 32 32 33 - 34 33 ## v? Portability 35 34 36 35 - [ ] Common background runtime ··· 59 58 - [x] Untagged -> default group 60 59 - [x] Cmd to open groups home 61 60 62 - - [ ] What does it mean for a group to be active/not 63 - - [ ] Open new empty page (in a group or not) 61 + - [ ] Use escapeMode to open pages "in an active group" (maybe depends on page metadata api?) 62 + - [ ] Escape for navigating back up the group views, not closing window 63 + - [ ] Be able to open new empty page (in a group or not) 64 64 65 65 Cmd 66 - - [ ] adaptive matching 67 - - [ ] frecency 66 + - [x] adaptive matching 67 + - [x] frecency 68 + 69 + ## v? Windowing model 70 + 71 + - [ ] active vs transient modality 72 + - [ ] configurable escape behavior per-window 68 73 69 74 ## V.0.3 - Datastore 75 + 76 + this needs a lot of work, but good enough for now. 77 + also, will be shaped as we move through the extensibility pieces. 70 78 71 79 - [x] Datastore 72 80
+2 -1
app/cmd/commands/groups.js
··· 118 118 width: 800, 119 119 height: 600, 120 120 trackingSource: 'cmd', 121 - trackingSourceId: 'groups' 121 + trackingSourceId: 'groups', 122 + escapeMode: 'navigate' // Allow internal navigation before closing 122 123 }); 123 124 } 124 125 },
+11 -7
app/groups/home.js
··· 32 32 untaggedCount: 0 33 33 }; 34 34 35 - // Handle ESC - go back to groups view 36 - document.onkeydown = (evt) => { 37 - if (evt.key === 'Escape') { 38 - if (state.view === VIEW_ADDRESSES) { 39 - showGroups(); 40 - } 35 + // Handle ESC - cooperative escape handling with window manager 36 + // Returns { handled: true } if we navigated internally 37 + // Returns { handled: false } if at root (groups list) and window should close 38 + api.escape.onEscape(() => { 39 + if (state.view === VIEW_ADDRESSES) { 40 + // Navigate back to groups list 41 + showGroups(); 42 + return { handled: true }; 41 43 } 42 - }; 44 + // At root (groups list) - let window close 45 + return { handled: false }; 46 + }); 43 47 44 48 const init = async () => { 45 49 debug && console.log('Groups init');
+65 -17
index.js
··· 919 919 try { 920 920 await win.loadURL(url); 921 921 922 + // Determine if this is a transient window (opened while no Peek window was focused) 923 + // Used for escapeMode: 'auto' to decide between navigate and close behavior 924 + const focusedWindow = BrowserWindow.getFocusedWindow(); 925 + const isTransient = !focusedWindow || focusedWindow.isDestroyed(); 926 + 922 927 // Add to window manager with modal parameter 923 928 const windowEntry = { 924 929 id: win.id, 925 930 source: msg.source, 926 931 params: { 927 932 ...options, 928 - address: url 933 + address: url, 934 + transient: isTransient 929 935 } 930 936 }; 931 937 console.log('Adding window to manager:', windowEntry.id, 'modal:', windowEntry.params.modal, 'keepLive:', windowEntry.params.keepLive); ··· 1810 1816 } 1811 1817 }; 1812 1818 1819 + // Ask renderer to handle escape, returns Promise<{ handled: boolean }> 1820 + const askRendererToHandleEscape = (bw) => { 1821 + return new Promise((resolve) => { 1822 + const responseChannel = `escape-response-${bw.id}-${Date.now()}`; 1823 + 1824 + // Timeout after 100ms - if renderer doesn't respond, assume not handled 1825 + const timeout = setTimeout(() => { 1826 + ipcMain.removeAllListeners(responseChannel); 1827 + resolve({ handled: false }); 1828 + }, 100); 1829 + 1830 + ipcMain.once(responseChannel, (event, response) => { 1831 + clearTimeout(timeout); 1832 + resolve(response || { handled: false }); 1833 + }); 1834 + 1835 + bw.webContents.send('escape-pressed', { responseChannel }); 1836 + }); 1837 + }; 1838 + 1813 1839 // esc handler 1814 - // TODO: make user-configurable 1840 + // Supports escapeMode: 'close' (default), 'navigate', 'auto' 1815 1841 const addEscHandler = bw => { 1816 1842 console.log('adding esc handler to window:', bw.id); 1817 - bw.webContents.on('before-input-event', (e, i) => { 1843 + bw.webContents.on('before-input-event', async (e, i) => { 1818 1844 if (i.key == 'Escape' && i.type == 'keyUp') { 1819 - // Get window info for better logging 1845 + // Get window info 1820 1846 const entry = windowManager.getWindow(bw.id); 1821 - const isSettingsWindow = entry && entry.params && entry.params.address === settingsAddress; 1822 - 1823 - console.log('===== Escape key pressed ====='); 1824 - console.log(`Window ID: ${bw.id}`); 1825 - console.log(`Is settings window: ${isSettingsWindow}`); 1826 - 1827 - if (entry && entry.params) { 1828 - console.log(`Window address: ${entry.params.address}`); 1829 - console.log(`Modal: ${entry.params.modal}, KeepLive: ${entry.params.keepLive}`); 1847 + const params = entry?.params || {}; 1848 + const escapeMode = params.escapeMode || 'close'; 1849 + 1850 + console.log(`ESC pressed - window ${bw.id}, escapeMode: ${escapeMode}`); 1851 + 1852 + // For 'navigate' mode, ask renderer first 1853 + if (escapeMode === 'navigate') { 1854 + const response = await askRendererToHandleEscape(bw); 1855 + console.log(`Renderer escape response:`, response); 1856 + 1857 + if (response.handled) { 1858 + // Renderer handled the escape (internal navigation) 1859 + console.log('Renderer handled escape, not closing'); 1860 + return; 1861 + } 1830 1862 } 1831 - 1832 - // Always trigger close/hide on Escape 1833 - console.log('Calling closeOrHideWindow...'); 1863 + 1864 + // For 'auto' mode, check if transient (no focused window when opened) 1865 + if (escapeMode === 'auto') { 1866 + if (params.transient) { 1867 + // Transient mode - close immediately 1868 + console.log('Auto mode (transient) - closing'); 1869 + } else { 1870 + // Active mode - ask renderer first 1871 + const response = await askRendererToHandleEscape(bw); 1872 + console.log(`Renderer escape response (auto/active):`, response); 1873 + 1874 + if (response.handled) { 1875 + console.log('Renderer handled escape, not closing'); 1876 + return; 1877 + } 1878 + } 1879 + } 1880 + 1881 + // Close or hide the window 1882 + console.log('Closing/hiding window'); 1834 1883 closeOrHideWindow(bw.id); 1835 - console.log('===== Escape handling complete ====='); 1836 1884 } 1837 1885 }); 1838 1886 };
+44
notes/commands.md
··· 1 + # Commands 2 + 3 + The cmd feature provides a command palette for executing actions, opening URLs, and interacting with the app. 4 + 5 + ## Command Matching & Sorting 6 + 7 + When the user types in the command bar, matching commands are sorted using a three-tier system: 8 + 9 + ### 1. Exact Match Priority 10 + 11 + When the input contains parameters (e.g., `tag foo`), an exact command match is prioritized over prefix matches. This ensures `tag foo` executes the `tag` command, not `tags`. 12 + 13 + ### 2. Adaptive Scoring 14 + 15 + The system learns from user behavior using asymptotic scoring: 16 + 17 + ``` 18 + score = count / (count + k) 19 + ``` 20 + 21 + Where: 22 + - `count` = number of times user selected this command for this typed prefix 23 + - `k` = dampening constant (currently 5) 24 + 25 + This creates ever-strengthening reinforcement: the more you select a command for a given input, the higher it ranks. The asymptotic curve means early selections have high impact, then it plateaus. 26 + 27 + Example: If you type "t" and select "tag" 10 times, the score is `10/(10+5) = 0.67`. After 100 selections: `100/(100+5) = 0.95`. 28 + 29 + Adaptive feedback is stored in localStorage per typed-prefix → command pairs. 30 + 31 + ### 3. Match Count (Frecency Fallback) 32 + 33 + If adaptive scores are equal, falls back to raw match count—how many times each command has been used overall. 34 + 35 + ## Implementation 36 + 37 + - `findMatchingCommands(text)` in `panel.js` handles matching and sorting 38 + - `updateAdaptiveFeedback(typed, name)` records user selections 39 + - `getAdaptiveScore(typed, name)` retrieves learned scores 40 + 41 + ## Storage Keys 42 + 43 + - `cmd:adaptiveFeedback` - typed prefix → command selection counts 44 + - `cmd:matchCounts` - overall command usage counts
+93
notes/escape-navigation.md
··· 1 + # Escape Navigation & Window Modes 2 + 3 + ## Modes 4 + 5 + ### Active Mode 6 + Peek is the focused app at the OS level. User is actively working within Peek. 7 + 8 + **Escape behavior**: Navigate back through internal state before closing. 9 + 10 + Example flow in Groups: 11 + 1. Groups list view 12 + 2. → Click group → Group detail view 13 + 3. → Click page → Page view 14 + 4. → ESC → Back to group detail 15 + 5. → ESC → Back to groups list 16 + 6. → ESC → Close groups window 17 + 18 + ### Transient Mode 19 + Peek was invoked via global hotkey while another app was active. User wants quick access then return to previous context. 20 + 21 + **Escape behavior**: Close window immediately, return focus to previous app. 22 + 23 + ## Detection 24 + 25 + How to determine mode: 26 + - Track whether Peek was active before window opened 27 + - If invoked via global shortcut while Peek wasn't focused → transient 28 + - If opened from within an existing Peek window → active 29 + 30 + ## API Changes 31 + 32 + Window open API could accept an `escapeMode` option: 33 + 34 + ```javascript 35 + api.window.open(url, { 36 + escapeMode: 'navigate' | 'close' | 'auto' 37 + }); 38 + ``` 39 + 40 + - `navigate`: ESC navigates back, only closes when at root 41 + - `close`: ESC immediately closes window 42 + - `auto`: Determine based on active vs transient mode (default) 43 + 44 + ## Implementation 45 + 46 + Cooperative model using IPC between main process and renderer: 47 + 48 + ### Main Process (index.js) 49 + 50 + 1. `addEscHandler()` intercepts ESC via `before-input-event` 51 + 2. For `escapeMode: 'navigate'` or `'auto'` (active), sends IPC `escape-pressed` to renderer 52 + 3. Waits 100ms for response via unique response channel 53 + 4. If renderer returns `{ handled: true }` → do nothing (renderer navigated) 54 + 5. If renderer returns `{ handled: false }` or timeout → close/hide window 55 + 56 + ### Preload API (preload.js) 57 + 58 + ```javascript 59 + api.escape.onEscape(callback) 60 + ``` 61 + 62 + Register a callback that's invoked on ESC. Callback should return: 63 + - `{ handled: true }` - ESC was handled (internal navigation occurred) 64 + - `{ handled: false }` - At root state, window should close 65 + 66 + ### Renderer Usage (e.g., groups/home.js) 67 + 68 + ```javascript 69 + api.escape.onEscape(() => { 70 + if (state.view === VIEW_ADDRESSES) { 71 + showGroups(); 72 + return { handled: true }; 73 + } 74 + return { handled: false }; 75 + }); 76 + ``` 77 + 78 + ### Transient Detection 79 + 80 + When a window is opened, main process checks `BrowserWindow.getFocusedWindow()`: 81 + - If no focused window → `transient: true` stored in window params 82 + - For `escapeMode: 'auto'`, transient windows close immediately on ESC 83 + 84 + ## Future: Pinned Windows 85 + 86 + Exception case: windows pinned to stay visible over all OS windows. These would: 87 + - Ignore transient mode 88 + - Have their own escape behavior (perhaps require explicit close action) 89 + 90 + ## Open Questions 91 + 92 + 1. How does this interact with browser-style back/forward within webviews? 93 + 2. Should there be visual indication of mode (transient vs active)?
+18
preload.js
··· 284 284 ipcRenderer.send('app-quit', { source: sourceAddress }); 285 285 }; 286 286 287 + // Escape handling API 288 + // For windows with escapeMode: 'navigate' or 'auto' 289 + // Callback should return { handled: true } if escape was handled internally 290 + // or { handled: false } to let the window close 291 + api.escape = { 292 + onEscape: (callback) => { 293 + ipcRenderer.on('escape-pressed', async (event, data) => { 294 + try { 295 + const result = await callback(); 296 + ipcRenderer.send(data.responseChannel, result || { handled: false }); 297 + } catch (err) { 298 + console.error('Error in escape handler:', err); 299 + ipcRenderer.send(data.responseChannel, { handled: false }); 300 + } 301 + }); 302 + } 303 + }; 304 + 287 305 // unused 288 306 /* 289 307 api.sendToWindow = (windowId, msg) => {