experiments in a post-browser web

peak peek, downhill from here

+1281 -22
+63 -22
README.md
··· 1 1 # Peek 2 2 3 - ![peek logo of eyeball peeking up from bottom](https://raw.githubusercontent.com/autonome/Peek/master/icons/banner.png) 3 + Please meet Peek, a web user agent application designed for using the web where, when and how you want it. 4 + 5 + ** WARNING** 6 + * Peek is not a web browser! There are no tabs, and no windows in the browser sense of them. If that's what you're looking for, there are a few decent browsers for you to choose from. 7 + * Peek is not safe for general use yet! It is a crude proof of concept I whipped up while on vacation. While I have thoughts on security model and user interface, I have not written it up into a proper security model yet. 8 + 9 + ## Features 10 + 11 + You can use Peek in three ways, with more coming: 12 + 13 + * Peeks - Keyboard activated modal chromeless web pages mapped to `Opt+0-9` 14 + * Slides - Gesture activated modal chromeless web pages which slide in from left/right/bottom/top 15 + * Scripts - Scripts periodically executed against a web page in the background which extract data and notify on changes 16 + 17 + ### Peeks 18 + 19 + Peeks are keyboard activated modal chromeless web pages mapped to `Opt+0-9` and closed on blur, the `Escape` key or `cmd/ctrl+w`. 20 + 21 + ### Slides 22 + 23 + Slides are gesture activated modal chromeless web pages which slide in from left/right/bottom/top, and closed on blur, the `Escape` key or `cmd/ctrl+w`. 24 + 25 + ### Scripts 26 + 27 + Scripts periodically load a web page in the background and extract data matching a CSS selector, stores it, and notify the user when the resulting data changes. 28 + 29 + Ok, so not really "scripts" yet. But safe and effective enough for now. 30 + 31 + ## Why 32 + 33 + Some thoughts driving the design of Peek: 34 + 35 + * Web user agents should be bounded by the user, not browser vendor business models 36 + * Windows and tabs should have died a long time ago, a mixed metaphor constraining the ability of the web to grow/thrive/change and meet user needs 37 + * Security user interface must be a clear articulation of risks and trade-offs, and users should own the decisions 38 + 39 + # Architecture / Implementation 4 40 5 - Quickly peek at your favorite web pages without breaking your flow. 41 + Peek is designed to be modular and configurable around the idea that parts of it can run in different environments. 42 + 43 + For example: 44 + * Definitely planning on a mobile app which syncs and runs your peeks/slides/scripts 45 + * I'd like to have a decentralized compute option for running your scripts outside of your clients and syncing the data 46 + * Want cloud storage for all config and data, esp infinite history, so can do fun things with it 6 47 7 - Peek lets you choose 10 web pages to open with a keyboard shortcut, without opening a new tab, and able to close with the `escape` key. 48 + ## Desktop App 8 49 9 - I use this for: 50 + Proof of concept is Electron. By far the best option today for cross-platform desktop apps which need a web rendering engine. There's really nothing else remotely suited (yet). 10 51 11 - * Translating text 12 - * See my Github notifications 13 - * Calendar schedule 14 - * Check email periodically instead of having it open all the time 15 - * Stock or cryptocurrency prices 16 - * Slack instances I don't want loaded all the time 17 - * Check the weather 52 + The user interface is just Tweakpane panels and modal chromeless web pages rn. 18 53 19 - ## Usage 54 + TODO 55 + * Need to look at whether could library-ize some of what Agregore implemented for non-HTTP protocol support. 56 + * Min browser might be interesting as a forkable base to work from and contribute to, if they're open to it. At least, should look more at the architecture. 20 57 21 - * Create up to 10 bookmarks with "peek#" plus the number 0-9 in the title. Eg "My favorite website peek#1". 58 + ## Mobile 22 59 23 - * Use the keyboard shortcut `alt+shift` plus the number you put in the bookmark title to peek at that URL. 60 + TBD 24 61 25 - * A new minimal window will open with your chosen URL loaded. 62 + ## Cloud 26 63 27 - * Hit the `escape` key to close the window. 64 + * Going full crypto payments for distributed compute on this one. 28 65 29 - (If no bookmark is configured for an index, Peek will just load about:home.) 66 + ## Future 30 67 31 - ## TODO 68 + * GCLI - not just a command bar, but like the Ubiquity extension 69 + * Lossless personal encrypted archive of web history 70 + * Implement the Firefox "awesomebar" scoring and search algorithm so that Peek *learns* you 71 + * Extension model designed for web user agent user interface experimentation 72 + * Panorama 32 73 33 - * ESC doesn't work sometimes 74 + ## History 34 75 35 - * test on windows/linux 76 + Peek was a browser extension that let you quickly peek at your favorite web pages without breaking your flow - loading pages mapped to keyboard shortcuts into a modal window with no controls, closable via the `Escape` key. 36 77 37 - * fix window to have a maximum size 78 + However, as browser extension APIs become increasingly limited, it was not possible to create a decent user experience and I abandoned it. You can access the extension in this repo [in the extension directory](/autonome/peek/extension/). 38 79 39 - * add feature to long-press links to peek (in pb mode?) 80 + The only way to create the ideal user experience for a web user agent that *Does What I Want* is to make it a browser-ish application, and that's what Peek is now. 40 81
assets/appicon.icns

This is a binary file and will not be displayed.

+128
assets/icons/AppIcon.appiconset/Contents.json
··· 1 + { 2 + "images":[ 3 + { 4 + "idiom":"iphone", 5 + "size":"20x20", 6 + "scale":"2x", 7 + "filename":"Icon-App-20x20@2x.png" 8 + }, 9 + { 10 + "idiom":"iphone", 11 + "size":"20x20", 12 + "scale":"3x", 13 + "filename":"Icon-App-20x20@3x.png" 14 + }, 15 + { 16 + "idiom":"iphone", 17 + "size":"29x29", 18 + "scale":"1x", 19 + "filename":"Icon-App-29x29@1x.png" 20 + }, 21 + { 22 + "idiom":"iphone", 23 + "size":"29x29", 24 + "scale":"2x", 25 + "filename":"Icon-App-29x29@2x.png" 26 + }, 27 + { 28 + "idiom":"iphone", 29 + "size":"29x29", 30 + "scale":"3x", 31 + "filename":"Icon-App-29x29@3x.png" 32 + }, 33 + { 34 + "idiom":"iphone", 35 + "size":"40x40", 36 + "scale":"2x", 37 + "filename":"Icon-App-40x40@2x.png" 38 + }, 39 + { 40 + "idiom":"iphone", 41 + "size":"40x40", 42 + "scale":"3x", 43 + "filename":"Icon-App-40x40@3x.png" 44 + }, 45 + { 46 + "idiom":"iphone", 47 + "size":"60x60", 48 + "scale":"2x", 49 + "filename":"Icon-App-60x60@2x.png" 50 + }, 51 + { 52 + "idiom":"iphone", 53 + "size":"60x60", 54 + "scale":"3x", 55 + "filename":"Icon-App-60x60@3x.png" 56 + }, 57 + { 58 + "idiom":"iphone", 59 + "size":"76x76", 60 + "scale":"2x", 61 + "filename":"Icon-App-76x76@2x.png" 62 + }, 63 + { 64 + "idiom":"ipad", 65 + "size":"20x20", 66 + "scale":"1x", 67 + "filename":"Icon-App-20x20@1x.png" 68 + }, 69 + { 70 + "idiom":"ipad", 71 + "size":"20x20", 72 + "scale":"2x", 73 + "filename":"Icon-App-20x20@2x.png" 74 + }, 75 + { 76 + "idiom":"ipad", 77 + "size":"29x29", 78 + "scale":"1x", 79 + "filename":"Icon-App-29x29@1x.png" 80 + }, 81 + { 82 + "idiom":"ipad", 83 + "size":"29x29", 84 + "scale":"2x", 85 + "filename":"Icon-App-29x29@2x.png" 86 + }, 87 + { 88 + "idiom":"ipad", 89 + "size":"40x40", 90 + "scale":"1x", 91 + "filename":"Icon-App-40x40@1x.png" 92 + }, 93 + { 94 + "idiom":"ipad", 95 + "size":"40x40", 96 + "scale":"2x", 97 + "filename":"Icon-App-40x40@2x.png" 98 + }, 99 + { 100 + "idiom":"ipad", 101 + "size":"76x76", 102 + "scale":"1x", 103 + "filename":"Icon-App-76x76@1x.png" 104 + }, 105 + { 106 + "idiom":"ipad", 107 + "size":"76x76", 108 + "scale":"2x", 109 + "filename":"Icon-App-76x76@2x.png" 110 + }, 111 + { 112 + "idiom":"ipad", 113 + "size":"83.5x83.5", 114 + "scale":"2x", 115 + "filename":"Icon-App-83.5x83.5@2x.png" 116 + }, 117 + { 118 + "size" : "1024x1024", 119 + "idiom" : "ios-marketing", 120 + "scale" : "1x", 121 + "filename" : "ItunesArtwork@2x.png" 122 + } 123 + ], 124 + "info":{ 125 + "version":1, 126 + "author":"makeappicon" 127 + } 128 + }
assets/icons/AppIcon.appiconset/Icon-App-20x20@1x.png

This is a binary file and will not be displayed.

assets/icons/AppIcon.appiconset/Icon-App-20x20@2x.png

This is a binary file and will not be displayed.

assets/icons/AppIcon.appiconset/Icon-App-20x20@3x.png

This is a binary file and will not be displayed.

assets/icons/AppIcon.appiconset/Icon-App-29x29@1x.png

This is a binary file and will not be displayed.

assets/icons/AppIcon.appiconset/Icon-App-29x29@2x.png

This is a binary file and will not be displayed.

assets/icons/AppIcon.appiconset/Icon-App-29x29@3x.png

This is a binary file and will not be displayed.

assets/icons/AppIcon.appiconset/Icon-App-40x40@1x.png

This is a binary file and will not be displayed.

assets/icons/AppIcon.appiconset/Icon-App-40x40@2x.png

This is a binary file and will not be displayed.

assets/icons/AppIcon.appiconset/Icon-App-40x40@3x.png

This is a binary file and will not be displayed.

assets/icons/AppIcon.appiconset/Icon-App-60x60@2x.png

This is a binary file and will not be displayed.

assets/icons/AppIcon.appiconset/Icon-App-60x60@3x.png

This is a binary file and will not be displayed.

assets/icons/AppIcon.appiconset/Icon-App-76x76@1x.png

This is a binary file and will not be displayed.

assets/icons/AppIcon.appiconset/Icon-App-76x76@2x.png

This is a binary file and will not be displayed.

assets/icons/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png

This is a binary file and will not be displayed.

assets/icons/AppIcon.appiconset/ItunesArtwork@2x.png

This is a binary file and will not be displayed.

+24
assets/icons/README.md
··· 1 + ## iTunesArtwork & iTunesArtwork@2x (App Icon) file extension: 2 + 3 + PNG extension is prepended to these two files - 4 + 5 + While Apple suggested to omit the extension for these files, 6 + the '.png' extension is actually required for iTunesConnect submission. 7 + 8 + This is done for you so you don't have to. 9 + 10 + However, for Ad_hoc or Enterprise distirbution, the extension should be removed 11 + from the files before adding to XCode to avoid error. 12 + 13 + refs: https://developer.apple.com/library/ios/qa/qa1686/_index.html 14 + 15 + ## iTunesArtwork & iTunesArtwork@2x (App Icon) transparency handling: 16 + 17 + As images with alpha channels or transparencies cannot be set as an application's icon on 18 + iTunesConnect, all transparent pixels in your images will be converted into 19 + solid blacks. 20 + 21 + To achieve the best result, you're advised to adjust the transparency settings 22 + in your source files before converting them with makeAppIcon. 23 + 24 + refs: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/AppIcons.html
assets/icons/iTunesArtwork@1x.png

This is a binary file and will not be displayed.

assets/icons/iTunesArtwork@2x.png

This is a binary file and will not be displayed.

assets/icons/iTunesArtwork@3x.png

This is a binary file and will not be displayed.

background.js extension/background.js
+17
config.js
··· 1 + const Store = require('electron-store'); 2 + 3 + const store = new Store(); 4 + store.clear(); 5 + 6 + store.set('prefs', { 7 + globalKeyCmd: 'CommandOrControl+Escape', 8 + peekKeyPrefix: 'Option+' 9 + }); 10 + 11 + store.set('peeks', [ 12 + ]); 13 + 14 + store.set('scripts', [ 15 + ]); 16 + 17 + module.exports = store;
+8
content-main.js
··· 1 + console.log('content-main'); 2 + 3 + setTimeout(() => { 4 + const s = "selector: '.percent > span:nth-child(2)"; 5 + const r = document.querySelector(s); 6 + const value = r ? r.textContent : null; 7 + console.log('cs val', value; 8 + }, 1000);
content-script.js extension/content-script.js
+231
defaults.js
··· 1 + 2 + const Store = require('electron-store'); 3 + 4 + const prefsSchema = { 5 + "$schema": "https://json-schema.org/draft/2020-12/schema", 6 + "$id": "peek.prefs.schema.json", 7 + "title": "Peek - prefs", 8 + "description": "Peek user preferences", 9 + "type": "object", 10 + "properties": { 11 + "globalKeyCmd": { 12 + "description": "Global OS hotkey to load app", 13 + "type": "string", 14 + "default": "CommandOrControl+Escape" 15 + }, 16 + "peekKeyPrefix": { 17 + "description": "Global OS hotkey prefix to trigger peeks - will be followed by 0-9", 18 + "type": "string", 19 + "default": "Option+" 20 + }, 21 + }, 22 + "required": [ "globalKeyCmd", "peekKeyPrefix"] 23 + }; 24 + 25 + const peekSchema = { 26 + "$schema": "https://json-schema.org/draft/2020-12/schema", 27 + "$id": "peek.peek.schema.json", 28 + "title": "Peek - page peek", 29 + "description": "Peek page peek", 30 + "type": "object", 31 + "properties": { 32 + "keyNum": { 33 + "description": "Number on keyboard to open this peek, 0-9", 34 + "type": "integer", 35 + "default": 0 36 + }, 37 + "title": { 38 + "description": "Name of the peek - user defined label", 39 + "type": "string", 40 + "default": "New Peek" 41 + }, 42 + "address": { 43 + "description": "URL to execute script against", 44 + "type": "string", 45 + "default": "https://example.com" 46 + }, 47 + "persistState": { 48 + "description": "Whether to persist local state or load page into empty container - defaults to false", 49 + "type": "boolean", 50 + "default": false 51 + }, 52 + "keepLive": { 53 + "description": "Whether to keep page alive in background or load fresh when triggered - defaults to false", 54 + "type": "boolean", 55 + "default": false 56 + }, 57 + "allowSound": { 58 + "description": "Whether to allow the page to emit sound or not (eg for background music player peeks - defaults to false", 59 + "type": "boolean", 60 + "default": false 61 + }, 62 + "height": { 63 + "description": "User-defined height of peek page", 64 + "type": "integer", 65 + "default": 600 66 + }, 67 + "width": { 68 + "description": "User-defined width of peek page", 69 + "type": "integer", 70 + "default": 800 71 + }, 72 + }, 73 + "required": [ "keyNum", "title", "address", "persistState", "keepLive", "allowSound", 74 + "height", "width" ] 75 + }; 76 + 77 + const scriptSchema = { 78 + "$schema": "https://json-schema.org/draft/2020-12/schema", 79 + "$id": "peek.script.schema.json", 80 + "title": "Peek - script", 81 + "description": "Peek background script", 82 + "type": "object", 83 + "properties": { 84 + "id": { 85 + "description": "The unique identifier for a script", 86 + "type": "string", 87 + "default": "peek:script:REPLACEME" 88 + }, 89 + "title": { 90 + "description": "Name of the script - user defined", 91 + "type": "string", 92 + "default": "New Script" 93 + }, 94 + "version": { 95 + "description": "Version number of the script", 96 + "type": "string", 97 + "default": "1.0.0" 98 + }, 99 + "address": { 100 + "description": "URL to execute script against", 101 + "type": "string", 102 + "default": "https://example.com" 103 + }, 104 + "selector": { 105 + "description": "CSS Selector for the script", 106 + "type": "string", 107 + "default": "body > h1" 108 + }, 109 + "value": { 110 + "description": "Which element property to return - currently 'textContent' is the only supported value", 111 + "type": "string", 112 + "default": "textContent" 113 + }, 114 + "interval": { 115 + "description": "How often to execute the script, in milliseconds - defaults to five minutes", 116 + "type": "integer", 117 + "default": 300000, 118 + "minimum": 0 119 + }, 120 + "storeHistory": { 121 + "description": "Whether to store historic values - defaults to false", 122 + "type": "boolean", 123 + "default": false 124 + }, 125 + }, 126 + "required": [ "id", "title", "address", "version", "selector", "value", 127 + "interval", "storeHistory" ] 128 + }; 129 + 130 + const schemas = { 131 + prefs: prefsSchema, 132 + peek: peekSchema, 133 + script: scriptSchema 134 + }; 135 + 136 + const fullSchema = { 137 + prefs: prefsSchema, 138 + peek: peekSchema, 139 + script: scriptSchema, 140 + peeks: { 141 + type: 'array', 142 + items: { 143 + type: 'peek' 144 + } 145 + }, 146 + scripts: { 147 + type: 'array', 148 + items: { 149 + type: 'script' 150 + } 151 + }, 152 + }; 153 + 154 + const defaults = { 155 + prefs: { 156 + globalKeyCmd: 'CommandOrControl+Escape', 157 + peekKeyPrefix: 'Option+' 158 + }, 159 + peeks: [ 160 + { 161 + keyNum: 0, 162 + title: 'localhost', 163 + address: 'http://localhost/', 164 + persistState: false, 165 + keepLive: false, 166 + allowSound: false, 167 + height: '', 168 + width: '', 169 + }, 170 + { 171 + keyNum: 1, 172 + title: 'everytimezone', 173 + address: 'https://everytimezone.com/', 174 + persistState: false, 175 + keepLive: false, 176 + allowSound: false, 177 + height: '', 178 + width: '', 179 + } 180 + ], 181 + scripts: [ 182 + { 183 + id: 'peek:script:localhost:test', 184 + title: 'localhost test', 185 + address: 'http://localhost/', 186 + version: '1', 187 + selector: 'body > h1', 188 + value: 'textContent', 189 + interval: 300000, 190 + storehistory: false 191 + }, 192 + ] 193 + }; 194 + 195 + const set = data => { 196 + store.set('prefs', data.prefs); 197 + store.set('peeks', data.peeks); 198 + store.set('scripts', data.scripts); 199 + }; 200 + 201 + const store = new Store({ 202 + // TODO: re-enable schemas 203 + //schema: fullSchema, 204 + watch: true 205 + }); 206 + 207 + // DEBUG 208 + store.clear(); 209 + 210 + const tmp = store.get('prefs'); 211 + if (!tmp) { 212 + console.log('initializing datastore'); 213 + store.set('prefs', defaults.prefs); 214 + store.set('peeks', defaults.peeks); 215 + store.set('scripts', defaults.scripts); 216 + } 217 + 218 + module.exports = { 219 + schemas, 220 + data: { 221 + get prefs() { return store.get('prefs'); }, 222 + get peeks() { return store.get('peeks'); }, 223 + get scripts() { return store.get('scripts'); } 224 + }, 225 + set, 226 + watch: fn => { 227 + store.onDidAnyChange(newData => { 228 + fn(newData) 229 + }); 230 + } 231 + };
+40
extension/README.md
··· 1 + # Peek 2 + 3 + ![peek logo of eyeball peeking up from bottom](https://raw.githubusercontent.com/autonome/Peek/master/icons/banner.png) 4 + 5 + Quickly peek at your favorite web pages without breaking your flow. 6 + 7 + Peek lets you choose 10 web pages to open with a keyboard shortcut, without opening a new tab, and able to close with the `escape` key. 8 + 9 + I use this for: 10 + 11 + * Translating text 12 + * See my Github notifications 13 + * Calendar schedule 14 + * Check email periodically instead of having it open all the time 15 + * Stock or cryptocurrency prices 16 + * Slack instances I don't want loaded all the time 17 + * Check the weather 18 + 19 + ## Usage 20 + 21 + * Create up to 10 bookmarks with "peek#" plus the number 0-9 in the title. Eg "My favorite website peek#1". 22 + 23 + * Use the keyboard shortcut `alt+shift` plus the number you put in the bookmark title to peek at that URL. 24 + 25 + * A new minimal window will open with your chosen URL loaded. 26 + 27 + * Hit the `escape` key to close the window. 28 + 29 + (If no bookmark is configured for an index, Peek will just load about:home.) 30 + 31 + ## TODO 32 + 33 + * ESC doesn't work sometimes 34 + 35 + * test on windows/linux 36 + 37 + * fix window to have a maximum size 38 + 39 + * add feature to long-press links to peek (in pb mode?) 40 +
+22
forge.config.js
··· 1 + module.exports = { 2 + packagerConfig: {}, 3 + rebuildConfig: {}, 4 + makers: [ 5 + { 6 + name: '@electron-forge/maker-squirrel', 7 + config: {}, 8 + }, 9 + { 10 + name: '@electron-forge/maker-zip', 11 + platforms: ['darwin'], 12 + }, 13 + { 14 + name: '@electron-forge/maker-deb', 15 + config: {}, 16 + }, 17 + { 18 + name: '@electron-forge/maker-rpm', 19 + config: {}, 20 + }, 21 + ], 22 + };
icons/_head.html extension/icons/_head.html
icons/apple-touch-icon-180x180.png extension/icons/apple-touch-icon-180x180.png
icons/banner.png extension/icons/banner.png
icons/browserconfig.xml extension/icons/browserconfig.xml
icons/browserconfig/tile150x150.png extension/icons/browserconfig/tile150x150.png
icons/browserconfig/tile310x150.png extension/icons/browserconfig/tile310x150.png
icons/browserconfig/tile310x310.png extension/icons/browserconfig/tile310x310.png
icons/browserconfig/tile70x70.png extension/icons/browserconfig/tile70x70.png
icons/favicon-16x16.png extension/icons/favicon-16x16.png
icons/favicon-32x32.png extension/icons/favicon-32x32.png
icons/favicon.ico extension/icons/favicon.ico
icons/peek.svg extension/icons/peek.svg
icons/pwa-192x192.png extension/icons/pwa-192x192.png
icons/pwa-512x512.png extension/icons/pwa-512x512.png
+336
index.js
··· 1 + // main.js 2 + (async () => { 3 + 4 + console.log('main'); 5 + 6 + // Modules to control application life and create native browser window 7 + const { 8 + electron, 9 + app, 10 + BrowserView, 11 + BrowserWindow, 12 + globalShortcut, 13 + ipcMain, 14 + Menu, 15 + nativeTheme, 16 + Tray 17 + } = require('electron'); 18 + 19 + const path = require('path'); 20 + 21 + const labels = { 22 + app: { 23 + title: 'Peek' 24 + }, 25 + tray: { 26 + tooltip: 'Click to open Peek' 27 + } 28 + }; 29 + 30 + // keep app out of dock and tab switcher 31 + app.dock.hide(); 32 + 33 + // load data 34 + let { data, schemas, set, watch } = require('./defaults'); 35 + 36 + const ICON_RELATIVE_PATH = 'assets/icons/AppIcon.appiconset/Icon-App-20x20@2x.png'; 37 + const ICON_PATH = path.join(__dirname, ICON_RELATIVE_PATH); 38 + 39 + const isDev = require('electron-is-dev'); 40 + 41 + if (isDev) { 42 + // Enable live reload for Electron too 43 + require('electron-reload')(__dirname, { 44 + // Note that the path to electron may vary according to the main file 45 + electron: require(`${__dirname}/node_modules/electron`) 46 + }); 47 + /* 48 + try { 49 + require('electron-reloader')(module); 50 + } catch {} 51 + */ 52 + } 53 + 54 + const unhandled = require('electron-unhandled'); 55 + unhandled(); 56 + 57 + // system dark mode handling 58 + ipcMain.handle('dark-mode:toggle', () => { 59 + if (nativeTheme.shouldUseDarkColors) { 60 + nativeTheme.themeSource = 'light'; 61 + } else { 62 + nativeTheme.themeSource = 'dark'; 63 + } 64 + return nativeTheme.shouldUseDarkColors 65 + }); 66 + 67 + ipcMain.handle('dark-mode:system', () => { 68 + nativeTheme.themeSource = 'system'; 69 + }); 70 + 71 + let _windows = []; 72 + let _peekWins = {}; 73 + 74 + let _win = null; 75 + const getMainWindow = () => { 76 + //console.log('getMainWindow', _win === null); 77 + if (_win === null) { 78 + _win = createMainWindow(); 79 + } 80 + return _win; 81 + }; 82 + 83 + const createMainWindow = () => { 84 + // Create the browser window. 85 + const mainWindow = new BrowserWindow({ 86 + width: 800, 87 + height: 600, 88 + webPreferences: { 89 + preload: path.join(__dirname, 'preload.js') 90 + } 91 + }); 92 + 93 + // and load the index.html of the app. 94 + mainWindow.loadFile('main.html'); 95 + 96 + // Open the DevTools. 97 + mainWindow.webContents.openDevTools() 98 + 99 + return mainWindow; 100 + }; 101 + 102 + // 103 + app.on('activate', () => { 104 + // On macOS it's common to re-create a window in the app when the 105 + // dock icon is clicked and there are no other windows open. 106 + if (BrowserWindow.getAllWindows().length === 0) { 107 + getMainWindow().show(); 108 + } 109 + }); 110 + 111 + const initTray = () => { 112 + const tray = new Tray(ICON_PATH); 113 + tray.setToolTip(labels.tray.tooltip); 114 + tray.on('click', () => { 115 + getMainWindow().show(); 116 + }); 117 + return tray; 118 + }; 119 + 120 + const execContentScript = (script, cb) => { 121 + const view = new BrowserView({ 122 + webPreferences: { 123 + // isolate content and do not persist it 124 + partition: Date.now() 125 + } 126 + }); 127 + 128 + //win.setBrowserView(view) 129 + //view.setBounds({ x: 0, y: 0, width: 300, height: 300 }) 130 + view.webContents.loadURL(script.address); 131 + 132 + const str = ` 133 + const s = "${script.selector}"; 134 + const r = document.querySelector(s); 135 + const value = r ? r.textContent : null; 136 + value; 137 + `; 138 + 139 + view.webContents.on('dom-ready', async () => { 140 + try { 141 + const r = await view.webContents.executeJavaScript(str); 142 + cb(r); 143 + } catch(ex) { 144 + console.error('cs exec error', ex); 145 + cb(null); 146 + } 147 + }); 148 + }; 149 + 150 + const initScripts = scripts => { 151 + return; 152 + // debounce me somehow so not shooting em all off 153 + // at once every time app starts 154 + scripts.forEach(script => { 155 + const r = execContentScript(script, (res) => { 156 + console.log('cs r', res); 157 + }); 158 + }); 159 + }; 160 + 161 + const initGlobalShortcuts = prefs => { 162 + // register global activation shortcut 163 + if (!globalShortcut.isRegistered(prefs.globalKeyCmd)) { 164 + const onActivate = () => { 165 + getMainWindow().show(); 166 + }; 167 + 168 + const ret = globalShortcut.register(prefs.globalKeyCmd, onActivate); 169 + 170 + if (!ret) { 171 + console.error('Unable to register global key command.') 172 + } 173 + } 174 + }; 175 + 176 + const showPeek = (peek) => { 177 + const height = peek.height || 600; 178 + const width = peek.width || 800; 179 + 180 + let win = null; 181 + 182 + const key = 'peek' + peek.keyNum; 183 + 184 + if (_peekWins[key]) { 185 + console.log('peek', peek.keyNum, 'using stored window'); 186 + win = _peekWins[key]; 187 + win.show(); 188 + } 189 + else { 190 + console.log('peek', peek.keyNum, 'creating new window'); 191 + win = new BrowserWindow({ 192 + height, 193 + width, 194 + center: true, 195 + skipTaskbar: true, 196 + autoHideMenuBar: true, 197 + titleBarStyle: 'hidden', 198 + webPreferences: { 199 + preload: path.join(__dirname, 'peek-preload.js'), 200 + // isolate content and do not persist it 201 + partition: Date.now() 202 + } 203 + }); 204 + } 205 + 206 + const onGoAway = () => { 207 + if (peek.keepLive) { 208 + _peekWins[key] = win; 209 + win.hide(); 210 + } 211 + else { 212 + win.destroy(); 213 + } 214 + } 215 + win.on('blur', onGoAway); 216 + win.on('close', onGoAway); 217 + 218 + /* 219 + const str = ` 220 + window.addEventListener('keyup', e => { 221 + if (e.key == 'Escape') { 222 + console.log('peek script esc'); 223 + } 224 + }); 225 + 1; 226 + `; 227 + 228 + win.webContents.on('dom-ready', async () => { 229 + try { 230 + const r = await win.webContents.executeJavaScript(str); 231 + console.log(r); 232 + } catch(ex) { 233 + console.error('cs exec error', ex); 234 + } 235 + }); 236 + */ 237 + 238 + //win.setBounds({ x: 0, y: 0, width, height }) 239 + win.loadURL(peek.address); 240 + }; 241 + 242 + const initPeeks = (cmdPrefix, peeks) => { 243 + peeks.forEach((p, i) => { 244 + if (!globalShortcut.isRegistered(cmdPrefix + `${i}`)) { 245 + const ret = globalShortcut.register(cmdPrefix + `${i}`, () => { 246 + showPeek(p); 247 + }); 248 + 249 + if (!ret) { 250 + console.error('Unable to register peek'); 251 + } 252 + } 253 + }); 254 + }; 255 + 256 + const initData = data => { 257 + // initialize prefs 258 + const prefs = data.prefs; 259 + initGlobalShortcuts(prefs); 260 + 261 + // initialize peeks 262 + const peeks = data.peeks; 263 + if (peeks.length > 0) { 264 + initPeeks(prefs.peekKeyPrefix, peeks); 265 + } 266 + 267 + // initialize scripts 268 + const scripts = data.scripts; 269 + if (scripts.length > 0) { 270 + initScripts(scripts); 271 + } 272 + }; 273 + 274 + const onReady = () => { 275 + // create main app window on app start 276 + const win = getMainWindow(); 277 + 278 + initData(data); 279 + 280 + initTray(); 281 + 282 + watch(newData => { 283 + initData(newData); 284 + getMainWindow().webContents.send('configchange', {}); 285 + }); 286 + }; 287 + 288 + app.whenReady().then(onReady); 289 + 290 + // when renderer is ready, send over user data 291 + ipcMain.on('getconfig', () => { 292 + getMainWindow().webContents.send('config', { 293 + data, 294 + schemas 295 + }); 296 + }); 297 + 298 + // listen for updates 299 + ipcMain.on('setconfig', (event, newData) => { 300 + // write to datastore 301 + set(newData); 302 + }); 303 + 304 + // ipc ESC handler 305 + ipcMain.on('esc', (event, title) => { 306 + console.log('esc'); 307 + const win = getMainWindow(); 308 + win.close(); 309 + _win = null; 310 + /* 311 + if (win.isVisible()) { 312 + console.log('win is visible, hide it'); 313 + win.hide(); 314 + } 315 + */ 316 + }); 317 + 318 + // Quit when all windows are closed, except on macOS. There, it's common 319 + // for applications and their menu bar to stay active until the user quits 320 + // explicitly with Cmd + Q. 321 + app.on('window-all-closed', () => { 322 + console.log('window-all-closed', process.platform); 323 + if (process.platform !== 'darwin') { 324 + onQuit(); 325 + } 326 + }); 327 + 328 + const onQuit = () => { 329 + console.log('onquit'); 330 + // Unregister all shortcuts on app close 331 + globalShortcut.unregisterAll(); 332 + 333 + app.quit(); 334 + }; 335 + 336 + })();
+19
main.css
··· 1 + body { 2 + font-family: -apple-system, BlinkMacSystemFont, helvetica neue, helvetica, sans-serif; 3 + font-feature-settings: "tnum"; 4 + font-size: 12.4px; 5 + font-variant-numeric: tabular-nums; 6 + } 7 + 8 + body > div { 9 + margin-bottom: 10px; 10 + } 11 + 12 + body > div > div { 13 + margin-bottom: 10px; 14 + } 15 + 16 + h1 { 17 + margin-top: 1px; 18 + margin-bottom: 2px; 19 + }
+30
main.html
··· 1 + <!--index.html--> 2 + 3 + <!DOCTYPE html> 4 + <html> 5 + <head> 6 + <meta charset="UTF-8"> 7 + <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> 8 + <meta http-equiv="Content-Security-Policy" content="script-src 'self';"> 9 + <title>peek</title> 10 + <link rel="stylesheet" href="main.css"> 11 + </head> 12 + <body> 13 + <div> 14 + <h1>peek</h1> 15 + </div> 16 + 17 + <div class="houseofpane"> 18 + </div> 19 + 20 + <div> 21 + Node.js <span id="node-version"></span><br> 22 + Chromium <span id="chrome-version"></span><br> 23 + Electron <span id="electron-version"></span><br> 24 + </div> 25 + 26 + <script type=module src="./node_modules/tweakpane/dist/tweakpane.js"></script> 27 + <script type=module src="./renderer.js"></script> 28 + 29 + </body> 30 + </html>
manifest.json extension/manifest.json
misc/peek.sketch extension/misc/peek.sketch
+30
package.json
··· 1 + { 2 + "name": "Peek", 3 + "version": "0.0.1", 4 + "description": "Peek is a web user agent for working with the web in a more agent-ish fashion than a browser.", 5 + "main": "index.js", 6 + "author": "dietrich ayala", 7 + "license": "MIT", 8 + "devDependencies": { 9 + "@electron-forge/cli": "^6.0.5", 10 + "@electron-forge/maker-deb": "^6.0.5", 11 + "@electron-forge/maker-rpm": "^6.0.5", 12 + "@electron-forge/maker-squirrel": "^6.0.5", 13 + "@electron-forge/maker-zip": "^6.0.5", 14 + "electron": "^23.1.2", 15 + "electron-is-dev": "^2.0.0", 16 + "electron-reload": "^2.0.0-alpha.1", 17 + "electron-reloader": "^1.2.3" 18 + }, 19 + "dependencies": { 20 + "electron-squirrel-startup": "^1.0.0", 21 + "electron-store": "^8.1.0", 22 + "electron-unhandled": "^4.0.1", 23 + "tweakpane": "^3.1.7" 24 + }, 25 + "scripts": { 26 + "start": "electron-forge start", 27 + "package": "electron-forge package", 28 + "make": "electron-forge make" 29 + } 30 + }
+11
peek-preload.js
··· 1 + 2 + 3 + console.log('peek preload'); 4 + 5 + /* 6 + window.addEventListener('keyup', e => { 7 + if (e.key == 'Escape') { 8 + console.log('peek preload esc'); 9 + } 10 + }); 11 + */
+43
preload.js
··· 1 + console.log('preload'); 2 + const {app, contextBridge, ipcRenderer} = require('electron') 3 + 4 + let api = {}; 5 + 6 + api.onConfigChange = callback => { 7 + ipcRenderer.on('configchange', (ev, msg) => { 8 + callback(msg); 9 + }); 10 + }; 11 + 12 + api.getConfig = new Promise((resolve, reject) => { 13 + // TODO: race potential 14 + ipcRenderer.once('config', (ev, msg) => { 15 + resolve(msg); 16 + }); 17 + ipcRenderer.send('getconfig'); 18 + }); 19 + 20 + api.setConfig = cfg => { 21 + //console.log('preload: setConfig', cfg); 22 + ipcRenderer.send('setconfig', cfg); 23 + }; 24 + 25 + contextBridge.exposeInMainWorld('app', api); 26 + 27 + window.addEventListener('DOMContentLoaded', () => { 28 + const replaceText = (selector, text) => { 29 + const element = document.getElementById(selector) 30 + if (element) element.innerText = text 31 + } 32 + 33 + for (const dependency of ['chrome', 'node', 'electron']) { 34 + replaceText(`${dependency}-version`, process.versions[dependency]) 35 + } 36 + }); 37 + 38 + window.addEventListener('keyup', e => { 39 + if (e.key == 'Escape') { 40 + ipcRenderer.send('esc', ''); 41 + } 42 + }); 43 +
+274
renderer.js
··· 1 + console.log('renderer'); 2 + 3 + // TODO: move to proper l10n 4 + const labels = { 5 + shortcutsPane: { 6 + paneTitle: 'Keyboard Shortcuts', 7 + globalKeyCmd: 'Global activation shortcut', 8 + peekKeyPrefix: 'Peek shortcut prefix', 9 + }, 10 + peeksPane: { 11 + paneTitle: 'Peeks', 12 + testBtn: 'Try', 13 + newFolder: 'New peek', 14 + addBtn: 'Add', 15 + delBtn: 'Delete', 16 + }, 17 + scriptsPane: { 18 + paneTitle: 'Scripts', 19 + testBtn: 'Try', 20 + newFolder: 'New script', 21 + addBtn: 'Add', 22 + delBtn: 'Delete', 23 + } 24 + }; 25 + 26 + // TODO: capture and internally navigate out of panes 27 + window.addEventListener('keyup', e => { 28 + //console.log('renderer', 'onkeyup', e); 29 + if (e.key == 'Escape') { 30 + //ipcRenderer.send('esc', ''); 31 + } 32 + }); 33 + 34 + // send changes back to main process 35 + // it will notify us when saved 36 + // and we'll reload entirely 😐 37 + const updateToMain = data => { 38 + console.log('renderer: updating to main', data); 39 + window.app.setConfig(data); 40 + }; 41 + 42 + const containerEl = document.querySelector('.houseofpane'); 43 + let panes = []; 44 + 45 + const init = cfg => { 46 + console.log('renderer: init'); 47 + console.log('renderer: cfg', cfg); 48 + 49 + // blow away panes if this is an update 50 + if (panes.length > 0) { 51 + panes.forEach(p => { 52 + p.dispose(); 53 + }); 54 + panes = []; 55 + } 56 + 57 + containerEl.replaceChildren(); 58 + 59 + // build panes and wire up change handlers 60 + let { data, schemas } = cfg; 61 + 62 + panes.push(initShortcutsPane(containerEl, labels.shortcutsPane, schemas.prefs, data.prefs, newPrefs => { 63 + data.prefs = newPrefs; 64 + updateToMain(data); 65 + })); 66 + 67 + panes.push(initPeeksPane(containerEl, labels.peeksPane, schemas.peek, data.peeks, newPeeks => { 68 + data.peeks = newPeeks; 69 + updateToMain(data); 70 + })); 71 + 72 + panes.push(initScriptsPane(containerEl, labels.scriptsPane, schemas.script, data.scripts, newScripts => { 73 + data.scripts = newScripts; 74 + updateToMain(data); 75 + })); 76 + }; 77 + 78 + // listen for data changes 79 + window.app.onConfigChange(() => { 80 + console.log('onconfigchange'); 81 + window.app.getConfig.then(init); 82 + }); 83 + 84 + // initialization: get data and load ui 85 + window.app.getConfig.then(init); 86 + 87 + const fillPaneFromSchema = (pane, labels, schema, data, onChange) => { 88 + const props = schema.properties; 89 + Object.keys(props).forEach(k => { 90 + // schema for property 91 + const s = props[k]; 92 + 93 + // value (or default) 94 + const v = 95 + (data && data.hasOwnProperty(k)) 96 + ? data[k] 97 + : props[k].default; 98 + 99 + const params = {}; 100 + const opts = {}; 101 + 102 + if (s.type == 'integer') { 103 + opts.step = 1; 104 + } 105 + 106 + params[k] = v; 107 + const input = pane.addInput(params, k, opts); 108 + // TODO: consider inline state management 109 + input.on('change', ev => { 110 + // TODO: validate against schema 111 + console.log('change', k, ev.value) 112 + //data[k] = ev.value; 113 + }); 114 + }); 115 + }; 116 + 117 + // TODO: fuckfuckfuck 118 + // https://github.com/cocopon/tweakpane/issues/431 119 + const exportPaneData = pane => { 120 + const children = pane.rackApi_.children.filter(p => p.children); 121 + const val = pane.rackApi_.children.filter(p => p.children).map(paneChild => { 122 + return paneChild.children.reduce((obj, field) => { 123 + const k = field.label; 124 + if (!k) { 125 + return obj; 126 + } 127 + 128 + let v = null; 129 + 130 + const input = field.element.querySelector('.tp-txtv_i') 131 + if (input) { 132 + v = input.value; 133 + } 134 + 135 + const checkbox = field.element.querySelector('.tp-ckbv_i'); 136 + if (checkbox) { 137 + v = checkbox.checked; 138 + } 139 + 140 + // TODO: drop fields not supported for now 141 + if (v) { 142 + obj[k] = v; 143 + } 144 + 145 + return obj; 146 + }, {}); 147 + }); 148 + return val; 149 + }; 150 + 151 + const initShortcutsPane = (container, labels, schema, prefs, onChange) => { 152 + const pane = new Tweakpane.Pane({ 153 + container: container, 154 + title: labels.paneTitle 155 + }); 156 + 157 + fillPaneFromSchema(pane, labels, schema, prefs); 158 + 159 + const update = (ev) => { 160 + // TODO: this won't work forever 161 + // gotta fix when tweakpane state export exists 162 + // also, gotta add accelerator validation 163 + prefs[ev.presetKey] = ev.value; 164 + onChange(prefs); 165 + }; 166 + 167 + // handle changes to existing entries 168 + pane.on('change', update); 169 + 170 + return pane; 171 + }; 172 + 173 + const initPeeksPane = (container, labels, schema, peeks, onChange) => { 174 + const pane = new Tweakpane.Pane({ 175 + container: container, 176 + title: labels.paneTitle 177 + }); 178 + 179 + const update = (all) => { 180 + const newData = exportPaneData(pane); 181 + // remove "new item" entry if not 182 + if (!all) { 183 + newData.pop(); 184 + } 185 + console.log(newData) 186 + onChange(newData); 187 + }; 188 + 189 + peeks.forEach(entry => { 190 + const folder = pane.addFolder({ 191 + title: entry.title, 192 + expanded: false 193 + }); 194 + 195 + const onChange = newEntry => { 196 + }; 197 + 198 + fillPaneFromSchema(folder, labels, schema, entry, onChange); 199 + 200 + // TODO: implement 201 + folder.addButton({title: labels.testBtn}); 202 + 203 + // TODO: implement 204 + const delBtn = folder.addButton({title: labels.delBtn}); 205 + delBtn.on('click', () => { 206 + //folder.dispose(); 207 + pane.remove(folder); 208 + // https://github.com/cocopon/tweakpane/issues/533 209 + update(); 210 + }); 211 + 212 + folder.on('change', update); 213 + }); 214 + 215 + // add new item entry 216 + const folder = pane.addFolder({ 217 + title: labels.newFolder 218 + }); 219 + 220 + fillPaneFromSchema(folder, labels, schema); 221 + 222 + const btn = pane.addButton({title: labels.addBtn}); 223 + 224 + // handle adds of new entries 225 + btn.on('click', () => { 226 + update(true); 227 + }); 228 + 229 + // handle changes to existing entries 230 + //pane.on('change', update); 231 + 232 + return pane; 233 + }; 234 + 235 + const initScriptsPane = (container, labels, schema, scripts, onChange) => { 236 + const pane = new Tweakpane.Pane({ 237 + container: container, 238 + title: labels.paneTitle 239 + }); 240 + 241 + scripts.forEach(entry => { 242 + const folder = pane.addFolder({ 243 + title: entry.title, 244 + expanded: false 245 + }); 246 + fillPaneFromSchema(folder, labels, schema, entry); 247 + // TODO: implement 248 + folder.addButton({title: labels.testBtn}); 249 + // TODO: implement 250 + folder.addButton({title: labels.delBtn}); 251 + }); 252 + 253 + const folder = pane.addFolder({ 254 + title: labels.newFolder 255 + }); 256 + 257 + fillPaneFromSchema(folder, labels, schema); 258 + 259 + const btn = pane.addButton({title: labels.addBtn}); 260 + 261 + const update = () => { 262 + const newData = exportPaneData(pane); 263 + onChange(newData); 264 + }; 265 + 266 + // handle adds of new entries 267 + btn.on('click', update); 268 + 269 + // handle changes to existing entries 270 + pane.on('change', update); 271 + 272 + return pane; 273 + }; 274 +
+5
scripts.js
··· 1 + // Manage content script execution 2 + 3 + const manager = { 4 + 5 + };