experiments in a post-browser web
10
fork

Configure Feed

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

Merge branch 'switch'

+1290 -714
+188 -70
README.md
··· 67 67 68 68 ## Design patterns 69 69 70 - - Escape IZUI 71 - * IZUI: inverse zooming user interface 72 - * ZUIs navigate by starting from a known root and user navigates by zooming ever further in 73 - * Escape starts anywhere, and instead of navigating by zooming in, all interfaces can zoom out to reset 74 - * allows unbounded and diverse entry points with predictable behavior 75 - * consistent path to familiar ground 70 + Escape IZUI 71 + * IZUI: inverse zooming user interface 72 + * ZUIs navigate by starting from a known root and user navigates by zooming ever further in 73 + * Escape starts anywhere, and instead of navigating by zooming in, all interfaces can zoom out to reset 74 + * allows unbounded and diverse entry points with predictable behavior 75 + * consistent path to familiar ground 76 76 77 77 Escape navigation model 78 - * navigation base can start at any level in stack 79 - * forward navigations are added on top of stack 80 - * backwards navigations walk the stack in reverse 78 + * navigation base can start at any level in stack 79 + * forward navigations are added on top of stack 80 + * backwards navigations walk the stack in reverse 81 81 82 82 ## Architecture / Implementation 83 83 84 - Peek is designed to be modular and configurable around the idea that parts of it can run in different environments. 84 + Peek is designed to be modular and configurable around the idea that parts of it 85 + can run in different environments. 85 86 86 87 For example: 87 88 - Definitely planning on a mobile app which syncs and runs your peeks/slides/scripts 88 89 - I'd like to have a decentralized compute option for running your scripts outside of your clients and syncing the data 89 90 - Want cloud storage for all config and data, esp infinite history, so can do fun things with it 90 91 92 + ### "features" == privileged web apps 93 + 94 + The core features are web apps loaded over a custom protocol: 95 + - currently with a scheme of `peek` 96 + - access to a few special apis noted in the next section 97 + 98 + ### Peek API 99 + 100 + Initially the prototype was all Electron. But that's not interesting, and doesn't 101 + really tell us anything about constraints of the web itself. 102 + 103 + So instead I asked this question: What's the minimum capability set that a web app would 104 + need to build the features I need? 105 + 106 + The answer, so far, is giving `peek` apps the following APIs: 107 + 108 + - window open/close 109 + - global hotkey registration 110 + - pubsub messaging 111 + 112 + Custom window api might be able to away entirely, by passing window.open features, working on that. 113 + 91 114 ### Desktop App 92 115 93 116 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). 94 117 95 - The user interface is just Tweakpane panels and modal chromeless web pages rn. 118 + User interface: 119 + - the built-in features are all modal chromeless web pages at this point 120 + - settings "apps" are [lil-gui](https://github.com/georgealways/lil-gui) panels 96 121 97 122 TODO 98 123 - Need to look at whether could library-ize some of what Agregore implemented for non-HTTP protocol support. ··· 119 144 120 145 ## Papercut / use-case log 121 146 147 + core 148 + - open a web page on top/bottom/left/right 149 + - keep web pages persistent in the background 150 + - quickly open a web page modally, and close it 151 + 122 152 - open bandcamp in a window, move over to 2nd display, accidently close it while moving around between other windows 123 153 - recent books or recipes from newsletters i subscribe to (but probably didn't read) 124 154 - extract a table from a page periodically, send it somewhere as csv or whatever (chained actions) ··· 127 157 - be able to see where a book/etc recommendation came from 128 158 - save a tweet, with URL / image / relevant text, but not whole page webrecorder style 129 159 160 + Content scripts 161 + - extract+log shazams 162 + - extract+log spotify playlist 163 + 164 + Workflow deconstructing a "why" task flavour of bookmarking 165 + - save https://www.criterionchannel.com/hong-kong-in-new-york 166 + - extract the movies 167 + - get reference metadata for each (?!) 168 + - add to "to watch list", with pointer back to source url 169 + 130 170 ## Roadmap 131 171 132 - ## v0.1 - MVP (minimum viable preview) 172 + ## v0.1 - MVPOC 173 + 174 + minimum viable proof of concept. 175 + 176 + question: would i use this? 133 177 134 178 Core moduluarization 135 179 - [x] Modularize feature types, eyeing the extensibility model ··· 174 218 - [x] basic command bar to open pages 175 219 - [x] fix setting layout wrapping issue 176 220 177 - Core blockers 178 - - [ ] built-in feature loading from origin not file 179 - - [ ] combine settings and background in built-in features? 180 - 181 221 Commands/messaging 182 222 - [x] implement pubsub api 183 223 - [x] way to tell feature to open default ui (if there is one) 184 224 - [x] way tell feature to open its settings ui (if there is one) 185 225 186 - Install/load/address features 187 - - [ ] app protocol? webextension? pwa? wtf? 188 - - [ ] pull from manifest (load/install via manifest with special key?) 189 - - [ ] manifests for feature metadata 190 - - [ ] feature urls? eg peek://settings(/index.html) 191 - - [ ] feature metadata in manifest 192 - - [ ] move feature bg pages to iframes in core bg page? 193 - 194 - Feature un/install and reloads 195 - - [x] feature unload/reload - init/uninit whole feature and window 196 - - [ ] all api calls have feature id accessible by preload (via manifest?) 197 - - [ ] unreg shortcuts on unload 198 - - confirm sucessful reg 199 - - send pubsub msgs on shortcut reg/unreg with feature id 200 - - in core/bg, listen for regs and map to feature 201 - - then on feature uninstall, unreg 202 - - [ ] close other windows, not just background (track all feature wins? hierarchy? window manager?) 203 - - [ ] figure out re-init/reload story when pref/feature change is saved 204 - - can leave to the apps? eg document.reload()? likely not for OS level stuff 205 - - could do a storage change listener, but all kinds of reasons why you *wouldn't* do full reload... 206 - - preload could register window + thing (eg kb listener) and listen for feature-disable events 207 - - ok so basically do at api level 208 - - [ ] language: call them feature or apps? other? 209 - - [ ] core settings re-render on feature toggle 210 - 211 - Daily driver blockers 212 - - [x] debug vs profile(s) for app dir 213 - - [ ] actually load/unload peeks when enabled/disabled 214 - - [ ] actually load/unload slides when enabled/disabled 215 - - [ ] actually load/unload scripts when enabled/disabled 216 - - [ ] fix ESC not working right 217 - - [ ] fix ESC not working in web content 218 - - [ ] make it so start feature can be unset 219 - 220 - Focus vs not focused app mode 221 - - [ ] openWindow option to not close on escape (perma windows w/ controls) 222 - - [ ] app focus detection in shortcuts 223 - - [ ] separate global shortcuts from app shortcuts (eg quit) 224 - - [ ] all-window show/hide when doing global shortcuts while app unfocused 225 - 226 226 Features cleanup 227 227 - [x] enable/disable individual slides, peeks 228 228 - [x] enable/disable individual scripts ··· 232 232 - [x] fix label names, match to pwa manifest 233 233 - [x] put readable log labels back in 234 234 235 - Dev niceties 236 - - [ ] figure out single devtools window if possible 235 + ## v0.2 - MVCP (minimum viable concept preview) 237 236 238 - Window controls/persistence/etc (after perma window) 239 - - [ ] window position persistence where it makes sense (settings, groups, cmd) and make configurable? 240 - - [ ] window size persistence where it makes sense (slides, peeks) and make configurable? 241 - - [ ] window controls 242 - - [ ] window resizers 237 + minimum viable concept preview. 243 238 244 - Window animations 245 - - [ ] add window open animation (to/from coords, time) to openWindow 246 - - [ ] update slides impl to use animation again 239 + question: would others use this? 247 240 248 - Window transparency 249 - - [ ] add support to api 250 - - [ ] update core settings to use it 251 - - [ ] update app settings to use it 241 + Windows/system 242 + - [x] app showing in dock even tho disabled 243 + - [x] app not showing in tray, even tho enabled 244 + - [x] all api calls get source attached 245 + - [x] window cache s/custom/map/ 246 + - [x] window cache all windows not just persistent 247 + - [x] window cache - evaluate key approach (use-case: apps need to identify windows they open) 248 + - [x] always return window id, so apps can manage it 249 + - [x] reimplement keys, so much easier for callers than managing ids 250 + - [x] account for number of renderer processes (seems double?) 251 + 252 + redo window system to be more webby 253 + - [x] prototype window.open 254 + - [x] evaluate webContents.setWindowOpenHandler 255 + - [x] stop using openWindow to show pre-existing hidden windows? 256 + - [x] can track web windows locally 257 + - [x] can identify web windows on both sides (key/name) 258 + - [x] add new custom api for windows superpowers 259 + - [x] collapse window opening to span both approaches 260 + - [x] finish converting all openWindow to window.open 261 + 262 + Feature lifecycle (un/install and reloads) 263 + - [x] feature unload/reload - init/uninit whole feature and window 264 + - [x] all api calls have address accessible by preload 265 + - [x] close other windows of feature, not just background window 266 + 267 + Feature re-init/reload when toggled 268 + - [x] main: track shortcuts by source, remove when unloaded 269 + - [x] main: track window sources 270 + - [x] main: close child windows when (before) closing source window 271 + 272 + Shortcut lifecycle 273 + - [x] main process should handle multiple registrations correctly 274 + - [x] send/track feature id/origin w/ each registration 275 + - [x] unreg shortcuts on unload 276 + 277 + Window features 278 + - [ ] add back in window features to window.open 279 + - [x] show/hide (keep alive) 280 + - [x] transparent 281 + - [ ] enable global window resize 282 + - [ ] add draggable as pref 283 + 284 + Features clean themselves up for lifecycle events 285 + - [ ] determine if new web-native windowing approach resolves this 286 + - [ ] load/unload peeks when enabled/disabled 287 + - [ ] load/unload slides when enabled/disabled 288 + - [ ] load/unload scripts when enabled/disabled 289 + 290 + Peeks/Slides 291 + - [x] only register shortcut and create window if a URL is configured 292 + - [ ] unreg shortcuts and close windows on peek un/configure 293 + - [ ] unreg shortcuts and close windows on slides un/configure 294 + 295 + Cmd 296 + - [x] fix it 297 + - [ ] app-scoped multi-window pages open 298 + 299 + Settings 300 + - [x] fix window size 301 + - [x] transparency 302 + - [ ] core settings re-render on feature toggle, eg feature-settings link enabled 303 + - [ ] default position (size to screen) 304 + 305 + Daily driver blockers 306 + - [x] debug vs profile(s) for app dir 307 + - [x] fix ESC not working right 308 + - [x] fix ESC not working in web content 309 + - [x] fix ESC not working right over lil-gui 310 + 311 + Dev stuff 312 + - [x] figure out single devtools window if possible 252 313 253 314 Deployment 254 315 - [ ] app updates 255 316 - [ ] icons 256 317 - [ ] about page 257 318 319 + Demo scenario 320 + - [ ] Peeks: translate, calendar, ai chat, currency conversion, everytimezone, tldraw 321 + - [ ] Slides: soundcloud, crypto prices, notepad, todo list 322 + - [ ] Scripts: eth price, weather change 323 + 258 324 ### v0.2 - extensibility / remember shit 259 325 326 + Backburner wishlist 327 + - [ ] window switching order algo 328 + 329 + DX papercuts 330 + - [ ] why crashing on reload main 331 + - [ ] devtools stealing focus 332 + - [ ] unified floating devtools 333 + 334 + Window features 335 + - [x] add transparency support to api 336 + - [ ] distentangle transparency and content-fit 337 + - [ ] add the rest of that shit 338 + 339 + App mgmt 340 + - [ ] uniform policy for feature id creation (lean on web/extensions) 341 + - [ ] collisions 342 + 343 + App dev 344 + - [ ] app model - web? extension? P/IWA? other? 345 + - [ ] shared libs, eg utils 346 + - [ ] language: call them feature or apps? other? 347 + 348 + Focus vs not focused app mode 349 + - [ ] openWindow option to not close on escape (perma windows w/ controls) 350 + - [ ] app focus detection in shortcuts 351 + - [ ] separate global shortcuts from app shortcuts (eg quit) 352 + - [ ] all-window show/hide when doing global shortcuts while app unfocused 353 + 354 + Install/load/address features 355 + - [x] built-in feature loading from origin not file 356 + - [x] app protocol? webextension? pwa? wtf? 357 + - [ ] combine settings and background in built-in features? 358 + - eg, features can have default ui + bg services? 359 + - [ ] pull from manifest (load/install via manifest with special key?) 360 + - [ ] manifests for feature metadata 361 + - [ ] feature urls? eg peek://settings(/index.html) 362 + - [ ] feature metadata in manifest 363 + - [ ] move feature bg pages to iframes in core bg page? 364 + 365 + Settings 366 + - [ ] make it so start feature can be unset (eh) 367 + 260 368 Navigation 261 369 - [ ] make izui stack manager (part of window mgr?) 262 370 - [ ] esc stack: from feature settings back to core settings 263 371 - [ ] add to izui stack (and ix w/ history?) 372 + 373 + Window animations 374 + - [ ] add window open animation (to/from coords, time) to openWindow 375 + - [ ] update slides impl to use animation again 376 + 377 + Window controls/persistence/etc (after perma window) 378 + - [ ] window position persistence where it makes sense (settings, groups, cmd) and make configurable? 379 + - [ ] window size persistence where it makes sense (slides, peeks) and make configurable? 380 + - [ ] window controls 381 + - [ ] window resizers 264 382 265 383 History 266 384 - [ ] push navigations out through pubsub?
+12 -13
features/cmd/background.js
··· 1 1 // cmd/background.js 2 2 3 3 import { id, labels, schemas, storageKeys, defaults } from './config.js'; 4 - import { log as l, openStore } from "../utils.js"; 5 - 6 - const log = function(...args) { l(labels.name, args); }; 4 + import { openStore, openWindow } from "./utils.js"; 7 5 8 - log('background', labels.name); 6 + console.log('background', labels.name); 9 7 10 8 const debug = window.app.debug; 11 9 const clear = false; ··· 13 11 const store = openStore(id, defaults, clear /* clear storage */); 14 12 const api = window.app; 15 13 14 + const address = 'peek://cmd/panel.html'; 15 + const settingsAddress = 'peek://cmd/settings.html'; 16 + 16 17 const openInputWindow = prefs => { 17 18 const height = prefs.height || 50; 18 19 const width = prefs.width || 600; 19 20 20 21 const params = { 21 22 debug, 22 - feature: labels.name, 23 - file: 'features/cmd/panel.html', 23 + address, 24 + key: address, 24 25 height, 25 26 width 26 27 }; 27 28 28 - api.openWindow(params); 29 + openWindow(address, params); 29 30 }; 30 31 31 32 const openSettingsWindow = (prefs) => { ··· 34 35 35 36 const params = { 36 37 debug, 37 - feature: labels.name, 38 - file: 'features/core/settings.html', 39 - height, 40 - width 38 + address: settingsAddress, 39 + transparent: true 41 40 }; 42 41 43 - _api.openWindow(params); 42 + window.open(settingsAddress, params); 44 43 }; 45 44 46 45 const initShortcut = (prefs) => { ··· 50 49 }; 51 50 52 51 const init = () => { 53 - log('init'); 52 + console.log('init'); 54 53 55 54 const prefs = () => store.get(storageKeys.PREFS); 56 55
+14 -21
features/cmd/commands.js
··· 1 - import { id, labels, schemas, ui, defaults } from './config.js'; 2 - import { log as l, openStore } from "../utils.js"; 1 + import { id, labels, schemas, storageKeys, defaults } from './config.js'; 2 + import { openStore, flattenObj } from "./utils.js"; 3 3 4 - const log = function(...args) { l(labels.name, args); }; 5 - 6 - log('background'); 4 + console.log('commands'); 7 5 8 6 const debug = window.app.debug; 9 - const store = openStore(id); 7 + const clear = false; 8 + 9 + const store = openStore(id, defaults, clear /* clear storage */); 10 10 const api = window.app; 11 11 12 - const storageKeys = { 13 - PREFS: 'prefs', 14 - ITEMS: 'items', 15 - }; 16 - 17 12 let commands = {}; 18 13 19 14 function onCommandsUpdated () { 20 15 window.dispatchEvent(new CustomEvent('cmd-update-commands', { detail: commands })); 21 - log('main sending updated commands out', Object.keys(commands)) 16 + console.log('main sending updated commands out', Object.keys(commands)) 22 17 } 23 18 24 19 window.addEventListener('DOMContentLoaded', initializeCommandSources); ··· 40 35 } 41 36 42 37 function initializeCommandSources() { 43 - log('initializeCommandSources'); 38 + console.log('initializeCommandSources'); 44 39 45 40 sourceOpenURL(); 46 41 //sourceBookmarklets(); ··· 67 62 68 63 const address = parts.shift(); 69 64 70 - const height = 600; 71 - const width = 800; 65 + if (!address) { 66 + return; 67 + } 72 68 73 69 const params = { 74 - feature: labels.name, 75 - address, 76 - height, 77 - width 70 + address 78 71 }; 79 72 80 - window.app.openWindow(params); 73 + window.open(address, '_blank', flattenObj(params)); 81 74 82 75 return { 83 - command: 'openWebWindow', 76 + command: 'open', 84 77 address 85 78 }; 86 79 }
+9 -11
features/cmd/panel.js
··· 38 38 39 39 */ 40 40 41 - import { id, labels, schemas, ui, defaults } from './config.js'; 42 - import { log as l, openStore } from "../utils.js"; 41 + import { id, labels, schemas, storageKeys, defaults } from './config.js'; 42 + import { log as l, openStore } from "./utils.js"; 43 43 44 - const log = function(...args) { l(labels.name, args); }; 45 - 46 - log('background'); 44 + console.log('panel'); 47 45 48 46 const debug = window.app.debug; 49 - const store = openStore(id); 47 + const clear = false; 48 + 49 + const store = openStore(id, defaults, clear /* clear storage */); 50 50 const api = window.app; 51 51 52 - const storageKeys = { 53 - PREFS: 'prefs', 54 - ITEMS: 'items', 55 - }; 52 + const address = 'peek://cmd/panel.html'; 53 + 56 54 57 55 let state = { 58 56 commands: [], // array of command names ··· 121 119 122 120 async function execute(name, typed) { 123 121 if (state.commands[name]) { 124 - log('executing cmd', name, typed); 122 + console.log('executing cmd', name, typed); 125 123 126 124 // execute command 127 125 const msg = state.commands[name].execute({typed});
+5 -3
features/cmd/settings.css
··· 3 3 font-feature-settings: "tnum"; 4 4 font-size: 12.4px; 5 5 font-variant-numeric: tabular-nums; 6 - margin: 12px; 6 + /*margin: 12px;*/ 7 7 } 8 8 9 + /* 9 10 body > div { 10 11 margin-bottom: 10px; 11 12 } ··· 23 24 justify-content: space-between; 24 25 columns: 2; 25 26 } 27 + */ 26 28 27 - /* tweakpanes */ 29 + /* lil-gui */ 28 30 .houseofpane > div { 29 31 min-width: 380px; 30 32 max-width: 380px; 31 - margin-bottom: 10px; 33 + /*margin-bottom: 10px;*/ 32 34 break-inside: avoid-column; 33 35 }
+5 -1
features/cmd/settings.html
··· 1 1 <!DOCTYPE html> 2 2 <html> 3 3 <head> 4 - <title>peek:core:settings</title> 4 + <title>peek:cmd:settings</title> 5 5 <meta charset="UTF-8"> 6 6 <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> 7 7 <meta http-equiv="Content-Security-Policy" content="script-src 'self';"> ··· 11 11 <link rel="stylesheet" href="node_modules/lil-gui/dist/lil-gui.min.css"> 12 12 </head> 13 13 <body> 14 + <!-- 14 15 <div> 15 16 <h1>peek</h1> 16 17 </div> 18 + --> 17 19 18 20 <div class="houseofpane"> 19 21 </div> 20 22 23 + <!-- 21 24 <div> 22 25 Node.js <span id="node-version"></span><br> 23 26 Chromium <span id="chrome-version"></span><br> 24 27 Electron <span id="electron-version"></span><br> 25 28 </div> 29 + --> 26 30 27 31 <script type=module src="./utils.js"></script> 28 32 <script type=module src="./config.js"></script>
+9 -15
features/cmd/utils.js
··· 1 1 const id = 'features/utils'; 2 2 3 - const log = (...args) => { 4 - if (!window.app.debug) { 5 - return; 6 - } 7 - 8 - const aargs = [...args]; 9 - const source = aargs.shift(); 10 - const str = aargs.map(JSON.stringify).join(', '); 11 - //const str = aargs.join(', '); 12 - console.log(str); 13 - window.app.log(source, str); 14 - }; 15 - 16 3 const openStore = (prefix, defaults, clear = false) => { 17 4 18 5 //log(id, 'openStore', prefix, (defaults ? Object.keys(defaults) : '')); ··· 88 75 return ctr; 89 76 } 90 77 78 + const flattenObj = o => Object.keys(o).map(k => `${k}=${o[k]}`).join(','); 79 + 80 + const openWindow = (address, params) => { 81 + const target = params.hasOwnProperty('key') ? params.key : '_blank'; 82 + return window.open(address, target, flattenObj(params)); 83 + }; 84 + 91 85 export { 92 - log, 93 86 openStore, 94 - addToGUI 87 + addToGUI, 88 + openWindow 95 89 };
+85 -51
features/core/background.js
··· 1 1 import { id, labels, schemas, storageKeys, defaults } from './config.js'; 2 - import { log as l, openStore } from "../utils.js"; 3 - 4 - const log = function(...args) { l(labels.name, args); }; 2 + import { openStore, openWindow } from "./utils.js"; 5 3 6 - log('background', labels.name); 4 + console.log('background', labels.name); 7 5 8 6 const debug = window.app.debug; 9 7 const clear = false; ··· 11 9 const store = openStore(id, defaults, clear /* clear storage */); 12 10 const api = window.app; 13 11 14 - const winKeyCache = new Map(); 12 + // maps app id to BrowserWindow id (background) 13 + const windows = new Map(); 14 + 15 + const settingsAddress = 'peek://core/settings.html'; 16 + const topicCorePrefs = 'topic:core:prefs'; 17 + const topicFeatureToggle = 'core:feature:toggle'; 18 + 19 + let _settingsWin = null; 15 20 16 21 const openSettingsWindow = (prefs) => { 22 + console.log('openSettingsWindow()'); 23 + 24 + /* 25 + // TODO: fuck, have to call main process to do this 26 + if (_settingsWin) { 27 + console.log('win exists, focusing'); 28 + _settingsWin.focus(); 29 + console.log('focused'); 30 + return; 31 + } 32 + */ 33 + 17 34 const height = prefs.height || 600; 18 - const width = prefs.width || 800; 35 + const width = prefs.width || 380; 19 36 20 37 const params = { 21 38 debug, 22 - feature: labels.name, 23 - address: 'peek://core/settings.html', 39 + address: settingsAddress, 40 + key: settingsAddress, 41 + transparent: true, 24 42 height, 25 43 width 26 44 }; 27 45 28 - api.openWindow(params); 46 + console.log('opening settings window', params); 47 + _settingsWin = openWindow(settingsAddress, params); 48 + console.log('opened settings window', _settingsWin); 29 49 }; 30 50 31 - const initShortcut = (prefs) => { 51 + const initSettingsShortcut = (prefs) => { 32 52 api.shortcuts.register(prefs.shortcutKey, () => { 53 + console.log('settings shortcut executed'); 33 54 openSettingsWindow(prefs); 34 55 }); 35 56 }; ··· 42 63 console.log('initializing feature ', f); 43 64 44 65 const params = { 45 - feature: f.name, 46 66 debug, 47 67 address: f.start_url, 68 + key: f.start_url, 48 69 keepLive: true, 49 - show: debug 70 + show: false 50 71 }; 51 72 52 - window.app.openWindow(params, r => { 53 - console.log(`initFeature(): win opened for ${f.name}`, r) 54 - winKeyCache.set(f.id, r.key); 55 - }); 56 - 57 - console.log('window opened'); 73 + const w = openWindow(f.start_url, params); 74 + windows.set(w, params); 58 75 }; 59 76 60 77 const uninitFeature = f => { 61 - 62 - const key = winKeyCache.get(f.id); 63 - if (key) { 78 + const wid = windows.get(f.id); 79 + if (wid) { 64 80 console.log('closing window for', f.name); 65 - window.app.closeWindow(key, r => { 81 + window.app.closeWindow(wid, r => { 66 82 console.log(`uninitFeature(): win closed for ${f.name}`, r) 67 - winKeyCache.delete(f.id); 83 + windows.delete(f.id); 68 84 }); 69 85 } 70 86 }; ··· 72 88 // unused, worth testing more tho 73 89 const initIframeFeature = file => { 74 90 const pathPrefix = 'file:///Users/dietrich/misc/peek/'; 75 - log('initiframe'); 91 + console.log('initiframe'); 76 92 const i = document.createElement('iframe'); 77 93 const src = pathPrefix + file; 78 - log('iframe src', src); 94 + console.log('iframe src', src); 79 95 document.body.appendChild(i); 80 96 i.src = src; 81 - log('iframe inited'); 97 + console.log('iframe inited'); 82 98 i.addEventListener('load', () => { 83 - log('iframe loaded'); 99 + console.log('iframe loaded'); 84 100 }); 85 101 }; 86 102 ··· 88 104 const features = () => store.get(storageKeys.FEATURES); 89 105 90 106 const init = () => { 91 - log('init'); 107 + console.log('init'); 92 108 93 109 const p = prefs(); 94 110 95 111 console.log('prefs', p); 96 112 97 - initShortcut(p); 98 - 99 - features().forEach(initFeature); 100 - //features.forEach(initIframeFeature); 101 - 102 - const startupFeatureTitle = p.startupFeature; 103 - 104 - const startupFeature = features().find(f => f.name = startupFeatureTitle); 113 + // main process uses these for initialization 114 + window.app.publish(topicCorePrefs, { 115 + id: id, 116 + prefs: p 117 + }, window.app.scopes.SYSTEM); 105 118 106 119 // Listen for system- or feature-level requests to open windows. 107 - // 108 - // In this case, for opening up global settings 109 - // on app start (if configured) and from the tray icon. 110 120 window.app.subscribe('open', msg => { 111 - if (msg.feature && msg.feature == 'feature/core/settings') { 121 + // eg from the tray icon. 122 + if (msg.address && msg.address == settingsAddress) { 112 123 openSettingsWindow(p); 113 124 } 114 125 }); 115 126 116 - // main process uses these for initialization 117 - window.app.publish('prefs', { 118 - feature: id, 119 - prefs: p 120 - }); 127 + if (p.startupFeature == settingsAddress) { 128 + openSettingsWindow(p); 129 + } 121 130 122 131 // feature enable/disable 123 - window.app.subscribe('core:feature:toggle', msg => { 132 + window.app.subscribe(topicFeatureToggle, msg => { 124 133 console.log('feature toggle', msg) 125 134 126 - const f = features().find(f => f.id = msg.featureId); 135 + const f = features().find(f => f.id == msg.featureId); 127 136 if (f) { 128 137 console.log('feature toggle', f); 129 138 if (msg.enabled == false) { ··· 139 148 console.log('feature toggle - no feature found for', f.name); 140 149 } 141 150 }); 151 + 152 + initSettingsShortcut(p); 153 + 154 + features().forEach(initFeature); 155 + 156 + //features.forEach(initIframeFeature); 157 + 158 + /* 159 + const addy = 'http://localhost'; 160 + const params = { 161 + debug, 162 + address: addy, 163 + key: addy, 164 + height: 300, 165 + width: 300 166 + }; 167 + 168 + const w = openWindow(addy, params); 169 + 170 + window.app.subscribe('onWindowOpened', msg => { 171 + api.modifyWindow(params.key, { 172 + hide: true 173 + }); 174 + }); 175 + */ 142 176 }; 143 177 144 178 window.addEventListener('load', init); ··· 151 185 const now = JSON.parse(e.newValue); 152 186 153 187 const featureKey = `${id}+${storageKeys.FEATURES}`; 154 - //log('onStorageChane', e.key, featureKey) 188 + //console.log('onStorageChane', e.key, featureKey) 155 189 if (e.key == featureKey) { 156 - //log('STORAGE CHANGE', e.key, old[0].enabled, now[0].enabled); 190 + //console.log('STORAGE CHANGE', e.key, old[0].enabled, now[0].enabled); 157 191 features().forEach((feat, i) => { 158 - log(feat.title, i, feat.enabled, old[i].enabled, now[i].enabled); 192 + console.log(feat.title, i, feat.enabled, old[i].enabled, now[i].enabled); 159 193 // disabled, so unload 160 194 if (old[i].enabled == true && now[i].enabled == false) { 161 195 // TODO 162 - log('TODO: add unloading of features', feat) 196 + console.log('TODO: add unloading of features', feat) 163 197 } 164 198 // enabled, so load 165 199 else if (old[i].enabled == false && now[i].enabled == true) {
+6 -6
features/core/config.js
··· 1 1 const id = '8aadaae5-2594-4968-aba0-707f0d371cfb'; 2 2 3 3 const labels = { 4 - name: 'Settings', 4 + name: 'Peek → Settings', 5 5 prefs: { 6 6 shortcutKey: 'Settings shortcut', 7 7 } ··· 10 10 const prefsSchema = { 11 11 "$schema": "https://json-schema.org/draft/2020-12/schema", 12 12 "$id": "peek.prefs.schema.json", 13 - "title": "Application Settings", 13 + "title": "Global Settings", 14 14 "description": "Peek user preferences", 15 15 "type": "object", 16 16 "properties": { ··· 30 30 "default": 800 31 31 }, 32 32 "startupFeature": { 33 - "description": "Feature to load at app startup", 33 + "description": "Address of what to load at startup, if anything", 34 34 "type": "string", 35 35 "default": "Settings" 36 36 }, ··· 108 108 prefs: { 109 109 shortcutKey: 'Option+,', 110 110 height: 850, 111 - width: 800, 112 - startupFeature: 'peek://core/settings', 111 + width: 380, 112 + startupFeature: 'peek://core/settings.html', 113 113 showTrayIcon: true, 114 - showInTrayAndSwitcher: true 114 + showInTrayAndSwitcher: false 115 115 }, 116 116 features: [ 117 117 { id: 'cee1225d-40ac-41e5-a34c-e2edba69d599',
+10 -3
features/core/settings.css
··· 3 3 font-feature-settings: "tnum"; 4 4 font-size: 12.4px; 5 5 font-variant-numeric: tabular-nums; 6 - margin: 12px; 6 + /*margin: 12px;*/ 7 7 } 8 8 9 + /* 9 10 body > div { 10 11 margin-bottom: 10px; 11 12 } ··· 23 24 justify-content: space-between; 24 25 columns: 2; 25 26 } 27 + */ 26 28 27 - /* tweakpanes */ 29 + .houseofpane { 30 + background-color: black; 31 + padding-top: 20px; 32 + } 33 + 34 + /* lil-gui */ 28 35 .houseofpane > div { 29 36 min-width: 380px; 30 37 max-width: 380px; 31 - margin-bottom: 10px; 38 + /*margin-bottom: 10px;*/ 32 39 break-inside: avoid-column; 33 40 }
+2
features/core/settings.html
··· 11 11 <link rel="stylesheet" href="node_modules/lil-gui/dist/lil-gui.min.css"> 12 12 </head> 13 13 <body> 14 + <!-- 14 15 <div> 15 16 <h1>peek</h1> 16 17 </div> 18 + --> 17 19 18 20 <div class="houseofpane"> 19 21 </div>
+9 -5
features/core/settings.js
··· 1 1 import { id, labels, schemas, storageKeys, defaults } from './config.js'; 2 - import { log as l, openStore, addToGUI } from "../utils.js"; 2 + import { openStore, addToGUI, openWindow} from "../utils.js"; 3 3 import GUI from './../../node_modules/lil-gui/dist/lil-gui.esm.min.js'; 4 4 5 - const log = function(...args) { l(labels.name, args); }; 6 5 const DEBUG = window.app.debug; 7 6 8 - log('loading', labels.name, 'settings'); 7 + console.log('loading', labels.name, 'settings'); 9 8 10 9 const store = openStore(id); 11 10 const container = document.querySelector('.houseofpane'); ··· 78 77 }; 79 78 80 79 const openSettingsAddress = (title, address) => { 80 + console.log('openSettingsAddress', title, address); 81 81 const params = { 82 - feature: title, 83 82 address, 83 + transparent: true 84 84 }; 85 85 86 - window.app.openWindow(params, () => window.app.log(title, 'settings win opened', address)); 86 + window.open(address, params); 87 87 } 88 88 89 89 window.addEventListener('load', init); 90 + 91 + window.addEventListener('blur', () => { 92 + console.log('core settings blur'); 93 + });
+9 -15
features/core/utils.js
··· 1 1 const id = 'features/utils'; 2 2 3 - const log = (...args) => { 4 - if (!window.app.debug) { 5 - return; 6 - } 7 - 8 - const aargs = [...args]; 9 - const source = aargs.shift(); 10 - const str = aargs.map(JSON.stringify).join(', '); 11 - //const str = aargs.join(', '); 12 - console.log(str); 13 - window.app.log(source, str); 14 - }; 15 - 16 3 const openStore = (prefix, defaults, clear = false) => { 17 4 18 5 //log(id, 'openStore', prefix, (defaults ? Object.keys(defaults) : '')); ··· 88 75 return ctr; 89 76 } 90 77 78 + const flattenObj = o => Object.keys(o).map(k => `${k}=${o[k]}`).join(','); 79 + 80 + const openWindow = (address, params) => { 81 + const target = params.hasOwnProperty('key') ? params.key : '_blank'; 82 + return window.open(address, target, flattenObj(params)); 83 + }; 84 + 91 85 export { 92 - log, 93 86 openStore, 94 - addToGUI 87 + addToGUI, 88 + openWindow 95 89 };
+8 -8
features/groups/background.js
··· 1 1 // groups/background.js 2 2 3 3 import { id, labels, schemas, storageKeys, defaults } from './config.js'; 4 - import { log as l, openStore } from "../utils.js"; 5 - 6 - const log = function(...args) { l(labels.name, args); }; 4 + import { openStore, openWindow } from "../utils.js"; 7 5 8 - log('background', labels.name); 6 + console.log('background', labels.name); 9 7 10 8 const debug = window.app.debug; 11 9 const clear = false; 12 10 13 11 const store = openStore(id, defaults, clear /* clear storage */); 14 12 const api = window.app; 13 + 14 + const address = 'features/groups/home.html'; 15 15 16 16 const openGroupsWindow = () => { 17 17 const height = 600; 18 18 const width = 800; 19 19 20 20 const params = { 21 - feature: labels.name, 22 - file: 'features/groups/home.html', 21 + address, 22 + key: address, 23 23 height, 24 24 width 25 25 }; 26 26 27 - api.openWindow(params); 27 + window.open(address, params); 28 28 }; 29 29 30 30 const initShortcut = shortcut => { ··· 46 46 }; 47 47 48 48 const init = () => { 49 - log('init'); 49 + console.log('init'); 50 50 51 51 const prefs = () => store.get(storageKeys.PREFS); 52 52
+5 -3
features/groups/settings.css
··· 3 3 font-feature-settings: "tnum"; 4 4 font-size: 12.4px; 5 5 font-variant-numeric: tabular-nums; 6 - margin: 12px; 6 + /*margin: 12px;*/ 7 7 } 8 8 9 + /* 9 10 body > div { 10 11 margin-bottom: 10px; 11 12 } ··· 23 24 justify-content: space-between; 24 25 columns: 2; 25 26 } 27 + */ 26 28 27 - /* tweakpanes */ 29 + /* lil-gui */ 28 30 .houseofpane > div { 29 31 min-width: 380px; 30 32 max-width: 380px; 31 - margin-bottom: 10px; 33 + /*margin-bottom: 10px;*/ 32 34 break-inside: avoid-column; 33 35 }
+5 -1
features/groups/settings.html
··· 1 1 <!DOCTYPE html> 2 2 <html> 3 3 <head> 4 - <title>peek:core:settings</title> 4 + <title>peek:groups:settings</title> 5 5 <meta charset="UTF-8"> 6 6 <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> 7 7 <meta http-equiv="Content-Security-Policy" content="script-src 'self';"> ··· 11 11 <link rel="stylesheet" href="node_modules/lil-gui/dist/lil-gui.min.css"> 12 12 </head> 13 13 <body> 14 + <!-- 14 15 <div> 15 16 <h1>peek</h1> 16 17 </div> 18 + --> 17 19 18 20 <div class="houseofpane"> 19 21 </div> 20 22 23 + <!-- 21 24 <div> 22 25 Node.js <span id="node-version"></span><br> 23 26 Chromium <span id="chrome-version"></span><br> 24 27 Electron <span id="electron-version"></span><br> 25 28 </div> 29 + --> 26 30 27 31 <script type=module src="./utils.js"></script> 28 32 <script type=module src="./config.js"></script>
+9 -15
features/groups/utils.js
··· 1 1 const id = 'features/utils'; 2 2 3 - const log = (...args) => { 4 - if (!window.app.debug) { 5 - return; 6 - } 7 - 8 - const aargs = [...args]; 9 - const source = aargs.shift(); 10 - const str = aargs.map(JSON.stringify).join(', '); 11 - //const str = aargs.join(', '); 12 - console.log(str); 13 - window.app.log(source, str); 14 - }; 15 - 16 3 const openStore = (prefix, defaults, clear = false) => { 17 4 18 5 //log(id, 'openStore', prefix, (defaults ? Object.keys(defaults) : '')); ··· 88 75 return ctr; 89 76 } 90 77 78 + const flattenObj = o => Object.keys(o).map(k => `${k}=${o[k]}`).join(','); 79 + 80 + const openWindow = (address, params) => { 81 + const target = params.hasOwnProperty('key') ? params.key : '_blank'; 82 + return window.open(address, target, flattenObj(params)); 83 + }; 84 + 91 85 export { 92 - log, 93 86 openStore, 94 - addToGUI 87 + addToGUI, 88 + openWindow 95 89 };
+5 -8
features/peeks/background.js
··· 1 1 // peeks/background.js 2 2 3 3 import { id, labels, schemas, storageKeys, defaults } from './config.js'; 4 - import { log as l, openStore } from "../utils.js"; 5 - 6 - const log = function(...args) { l(labels.name, args); }; 4 + import { openStore, openWindow } from "../utils.js"; 7 5 8 - log('background', labels.name); 6 + console.log('background', labels.name); 9 7 10 8 const debug = window.app.debug; 11 9 const clear = false; ··· 25 23 26 24 // peek 27 25 feature: labels.name, 28 - windowKey: `${labels.name}:${item.keyNum}`, 29 26 keepLive: item.keepLive || false, 30 - persistData: item.persistData || false 27 + persistState: item.persistState || false 31 28 }; 32 29 33 - api.openWindow(params); 30 + window.open(item.address, params); 34 31 }; 35 32 36 33 const initItems = (prefs, items) => { ··· 49 46 }; 50 47 51 48 const init = () => { 52 - log('init'); 49 + console.log('init'); 53 50 54 51 const prefs = () => store.get(storageKeys.PREFS); 55 52 const items = () => store.get(storageKeys.ITEMS);
+5 -3
features/peeks/settings.css
··· 3 3 font-feature-settings: "tnum"; 4 4 font-size: 12.4px; 5 5 font-variant-numeric: tabular-nums; 6 - margin: 12px; 6 + /*margin: 12px;*/ 7 7 } 8 8 9 + /* 9 10 body > div { 10 11 margin-bottom: 10px; 11 12 } ··· 23 24 justify-content: space-between; 24 25 columns: 2; 25 26 } 27 + */ 26 28 27 - /* tweakpanes */ 29 + /* lil-gui */ 28 30 .houseofpane > div { 29 31 min-width: 380px; 30 32 max-width: 380px; 31 - margin-bottom: 10px; 33 + /*margin-bottom: 10px;*/ 32 34 break-inside: avoid-column; 33 35 }
+5 -1
features/peeks/settings.html
··· 1 1 <!DOCTYPE html> 2 2 <html> 3 3 <head> 4 - <title>peek:core:settings</title> 4 + <title>peek:peeks:settings</title> 5 5 <meta charset="UTF-8"> 6 6 <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> 7 7 <meta http-equiv="Content-Security-Policy" content="script-src 'self';"> ··· 11 11 <link rel="stylesheet" href="node_modules/lil-gui/dist/lil-gui.min.css"> 12 12 </head> 13 13 <body> 14 + <!-- 14 15 <div> 15 16 <h1>peek</h1> 16 17 </div> 18 + --> 17 19 18 20 <div class="houseofpane"> 19 21 </div> 20 22 23 + <!-- 21 24 <div> 22 25 Node.js <span id="node-version"></span><br> 23 26 Chromium <span id="chrome-version"></span><br> 24 27 Electron <span id="electron-version"></span><br> 25 28 </div> 29 + --> 26 30 27 31 <script type=module src="./utils.js"></script> 28 32 <script type=module src="./config.js"></script>
+6 -6
features/peeks/settings.js
··· 1 + console.log('peek settings'); 1 2 import { id, labels, schemas, storageKeys, defaults } from './config.js'; 2 - import { log as l, openStore, addToGUI } from "../utils.js"; 3 + import { openStore, addToGUI } from "../utils.js"; 3 4 import GUI from './../../node_modules/lil-gui/dist/lil-gui.esm.min.js'; 4 5 5 - const log = function(...args) { l(labels.name, args); }; 6 - 7 - log('loading', labels.name, 'settings'); 6 + console.log('loading', labels.name, 'settings'); 8 7 9 - const debug = window.app.debug; 8 + //console.log('app', window.app); 9 + //const debug = window.app.debug; 10 10 const clear = false; 11 11 12 12 const store = openStore(id, defaults, clear /* clear storage */); 13 - const api = window.app; 13 + //const api = window.app; 14 14 15 15 const container = document.querySelector('.houseofpane'); 16 16 let prefs = store.get(storageKeys.PREFS);
+11 -15
features/peeks/utils.js
··· 1 1 const id = 'features/utils'; 2 2 3 - const log = (...args) => { 4 - if (!window.app.debug) { 5 - return; 6 - } 7 - 8 - const aargs = [...args]; 9 - const source = aargs.shift(); 10 - const str = aargs.map(JSON.stringify).join(', '); 11 - //const str = aargs.join(', '); 12 - console.log(str); 13 - window.app.log(source, str); 14 - }; 15 - 16 3 const openStore = (prefix, defaults, clear = false) => { 17 4 18 5 //log(id, 'openStore', prefix, (defaults ? Object.keys(defaults) : '')); ··· 37 24 clear: () => localStorage.clear() 38 25 }; 39 26 27 + /* 40 28 if (window.app.debug 41 29 && window.app.debugLevel == window.app.debugLevels.FIRST_RUN) { 42 30 log(id, 'openStore(): clearing storage') 43 31 store.clear(); 44 32 } 33 + */ 45 34 46 35 if (clear) { 47 36 store.clear(); ··· 88 77 return ctr; 89 78 } 90 79 80 + const flattenObj = o => Object.keys(o).map(k => `${k}=${o[k]}`).join(','); 81 + 82 + const openWindow = (address, params) => { 83 + const target = params.hasOwnProperty('key') ? params.key : '_blank'; 84 + return window.open(address, target, flattenObj(params)); 85 + }; 86 + 91 87 export { 92 - log, 93 88 openStore, 94 - addToGUI 89 + addToGUI, 90 + openWindow 95 91 };
+6 -9
features/scripts/background.js
··· 1 1 // scripts/background.js 2 2 3 3 import { id, labels, schemas, storageKeys, defaults } from './config.js'; 4 - import { log as l, openStore } from "../utils.js"; 5 - 6 - const log = function(...args) { l(labels.name, args); }; 4 + import { openStore, openWindow } from "../utils.js"; 7 5 8 - log('background', labels.name); 6 + console.log('background', labels.name); 9 7 10 8 const debug = window.app.debug; 11 9 const clear = false; ··· 25 23 `; 26 24 27 25 const params = { 28 - feature: labels.name, 29 26 address: script.address, 30 27 show: false, 31 28 script: { ··· 35 32 } 36 33 }; 37 34 38 - api.openWindow(params, cb); 35 + window.open(script.address, params); 39 36 }; 40 37 41 38 const initItems = (prefs, items) => { ··· 51 48 const interval = setInterval(() => { 52 49 const r = executeItem(item, res => { 53 50 54 - log('script result for', item.title, JSON.stringify(res)); 55 - log('script prev val', item.previousValue); 51 + console.log('script result for', item.title, JSON.stringify(res)); 52 + console.log('script prev val', item.previousValue); 56 53 57 54 if (item.previousValue != res) { 58 55 59 - log('result changed!', item.title, item.previousValue, res); 56 + console.log('result changed!', item.title, item.previousValue, res); 60 57 // TODO: figure this out - it blows away all timers, which isn't great 61 58 // 62 59 // update stored value
+5 -3
features/scripts/settings.css
··· 3 3 font-feature-settings: "tnum"; 4 4 font-size: 12.4px; 5 5 font-variant-numeric: tabular-nums; 6 - margin: 12px; 6 + /*margin: 12px;*/ 7 7 } 8 8 9 + /* 9 10 body > div { 10 11 margin-bottom: 10px; 11 12 } ··· 23 24 justify-content: space-between; 24 25 columns: 2; 25 26 } 27 + */ 26 28 27 - /* tweakpanes */ 29 + /* lil-gui */ 28 30 .houseofpane > div { 29 31 min-width: 380px; 30 32 max-width: 380px; 31 - margin-bottom: 10px; 33 + /*margin-bottom: 10px;*/ 32 34 break-inside: avoid-column; 33 35 }
+5 -1
features/scripts/settings.html
··· 1 1 <!DOCTYPE html> 2 2 <html> 3 3 <head> 4 - <title>peek:core:settings</title> 4 + <title>peek:scripts:settings</title> 5 5 <meta charset="UTF-8"> 6 6 <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> 7 7 <meta http-equiv="Content-Security-Policy" content="script-src 'self';"> ··· 11 11 <link rel="stylesheet" href="node_modules/lil-gui/dist/lil-gui.min.css"> 12 12 </head> 13 13 <body> 14 + <!-- 14 15 <div> 15 16 <h1>peek</h1> 16 17 </div> 18 + --> 17 19 18 20 <div class="houseofpane"> 19 21 </div> 20 22 23 + <!-- 21 24 <div> 22 25 Node.js <span id="node-version"></span><br> 23 26 Chromium <span id="chrome-version"></span><br> 24 27 Electron <span id="electron-version"></span><br> 25 28 </div> 29 + --> 26 30 27 31 <script type=module src="./utils.js"></script> 28 32 <script type=module src="./config.js"></script>
+9 -15
features/scripts/utils.js
··· 1 1 const id = 'features/utils'; 2 2 3 - const log = (...args) => { 4 - if (!window.app.debug) { 5 - return; 6 - } 7 - 8 - const aargs = [...args]; 9 - const source = aargs.shift(); 10 - const str = aargs.map(JSON.stringify).join(', '); 11 - //const str = aargs.join(', '); 12 - console.log(str); 13 - window.app.log(source, str); 14 - }; 15 - 16 3 const openStore = (prefix, defaults, clear = false) => { 17 4 18 5 //log(id, 'openStore', prefix, (defaults ? Object.keys(defaults) : '')); ··· 88 75 return ctr; 89 76 } 90 77 78 + const flattenObj = o => Object.keys(o).map(k => `${k}=${o[k]}`).join(','); 79 + 80 + const openWindow = (address, params) => { 81 + const target = params.hasOwnProperty('key') ? params.key : '_blank'; 82 + return window.open(address, target, flattenObj(params)); 83 + }; 84 + 91 85 export { 92 - log, 93 86 openStore, 94 - addToGUI 87 + addToGUI, 88 + openWindow 95 89 };
+24 -30
features/slides/background.js
··· 1 1 // slides/slides.js 2 2 3 3 import { id, labels, schemas, storageKeys, defaults } from './config.js'; 4 - import { log as l, openStore } from "../utils.js"; 5 - 6 - const log = function(...args) { l(labels.name, args); }; 4 + import { openStore, openWindow } from "../utils.js"; 7 5 8 - log('background', labels.name); 6 + console.log('background', labels.name); 9 7 10 8 const debug = window.app.debug; 11 9 const clear = false; ··· 14 12 const api = window.app; 15 13 16 14 const executeItem = (item) => { 17 - let height = item.height || 600; 18 - let width = item.width || 800; 15 + const height = item.height || 600; 16 + const width = item.width || 800; 19 17 20 - const size = { 18 + const screen = { 21 19 height: window.screen.height, 22 20 width: window.screen.width 23 21 }; ··· 27 25 switch(item.screenEdge) { 28 26 case 'Up': 29 27 // horizontally center 30 - x = (size.width - item.width) / 2; 28 + x = (screen.width - width) / 2; 31 29 32 30 // y starts at screen top and stays there 33 31 y = 0; 34 32 35 - width = item.width; 33 + //width = item.width; 36 34 //height = 1; 37 35 break; 38 36 case 'Down': 39 37 // horizonally center 40 - x = (size.width - item.width) / 2; 38 + x = (screen.width - item.width) / 2; 41 39 42 40 // y ends up at window height from bottom 43 41 // 44 - // eg: y = size.height - item.height; 42 + // eg: y = screen.height - item.height; 45 43 // 46 44 // but starts at screen bottom 47 - y = size.height; 45 + y = screen.height; 48 46 49 - width = item.width; 47 + //width = item.width; 50 48 //height = 1; 51 49 break; 52 50 case 'Left': ··· 55 53 x = 0; 56 54 57 55 // vertically center 58 - y = (size.height - item.height) / 2; 56 + y = (screen.height - item.height) / 2; 59 57 60 58 //width = 1; 61 - height = item.height; 59 + //height = item.height; 62 60 break; 63 61 case 'Right': 64 62 // x ends at at right screen edge - window size 65 63 // 66 - // eg: x = size.width - item.width; 64 + // eg: x = screen.width - item.width; 67 65 // 68 66 // but starts at screen right edge, will animate in 69 - x = size.width; 67 + x = screen.width; 70 68 71 69 // vertically center 72 - y = (size.height - item.height) / 2; 70 + y = (screen.height - item.height) / 2; 73 71 74 72 //width = 1; 75 - height = item.height; 73 + //height = item.height; 76 74 break; 77 75 default: 78 76 center = true; 79 77 console.log('waddafa'); 80 78 } 81 79 82 - log(item.screenEdge, x, y); 80 + console.log(item.screenEdge, x, y); 83 81 84 - const key = `${item.screenEdge}:${item.address}`; 82 + const key = `${item.address}:${item.screenEdge}`; 85 83 86 84 //animateSlide(win, item).then(); 87 85 88 86 const params = { 89 - // browserwindow 90 87 address: item.address, 91 88 height, 92 89 width, 90 + key, 93 91 94 - // peek 95 92 feature: labels.name, 96 - windowKey: `${labels.name}:${item.screenEdge}`, 97 93 keepLive: item.keepLive || false, 98 - persistData: item.persistData || false, 94 + persistState: item.persistState || false, 99 95 100 - // slide 101 96 x, 102 97 y, 103 - key, 104 98 }; 105 99 106 - api.openWindow(params); 100 + window.open(item.address, params); 107 101 }; 108 102 109 103 const initItems = (prefs, items) => { 110 - log('initItems'); 104 + console.log('initItems'); 111 105 const cmdPrefix = prefs.shortcutKeyPrefix; 112 106 113 107 items.forEach(item => { ··· 122 116 }; 123 117 124 118 const init = () => { 125 - log('init'); 119 + console.log('init'); 126 120 127 121 const prefs = () => store.get(storageKeys.PREFS); 128 122 const items = () => store.get(storageKeys.ITEMS);
+5 -3
features/slides/settings.css
··· 3 3 font-feature-settings: "tnum"; 4 4 font-size: 12.4px; 5 5 font-variant-numeric: tabular-nums; 6 - margin: 12px; 6 + /*margin: 12px;*/ 7 7 } 8 8 9 + /* 9 10 body > div { 10 11 margin-bottom: 10px; 11 12 } ··· 23 24 justify-content: space-between; 24 25 columns: 2; 25 26 } 27 + */ 26 28 27 - /* tweakpanes */ 29 + /* lil-gui */ 28 30 .houseofpane > div { 29 31 min-width: 380px; 30 32 max-width: 380px; 31 - margin-bottom: 10px; 33 + /*margin-bottom: 10px;*/ 32 34 break-inside: avoid-column; 33 35 }
+6 -2
features/slides/settings.html
··· 1 1 <!DOCTYPE html> 2 2 <html> 3 3 <head> 4 - <title>peek:core:settings</title> 4 + <title>peek:slides:settings</title> 5 5 <meta charset="UTF-8"> 6 6 <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> 7 7 <meta http-equiv="Content-Security-Policy" content="script-src 'self';"> ··· 11 11 <link rel="stylesheet" href="node_modules/lil-gui/dist/lil-gui.min.css"> 12 12 </head> 13 13 <body> 14 + <!-- 14 15 <div> 15 16 <h1>peek</h1> 16 17 </div> 18 + --> 17 19 18 20 <div class="houseofpane"> 19 21 </div> 20 - 22 + 23 + <!-- 21 24 <div> 22 25 Node.js <span id="node-version"></span><br> 23 26 Chromium <span id="chrome-version"></span><br> 24 27 Electron <span id="electron-version"></span><br> 25 28 </div> 29 + --> 26 30 27 31 <script type=module src="./utils.js"></script> 28 32 <script type=module src="./config.js"></script>
+2 -1
features/slides/settings.js
··· 63 63 64 64 // Add items 65 65 items.forEach((item, i) => { 66 + console.log('adding slide', item); 66 67 const folder = gui.addFolder(item.title); 67 68 68 - addToGUI(folder, 'Key mapping', item.keyNum).disable(); 69 + addToGUI(folder, 'Screen edge', item.screenEdge).disable(); 69 70 addToGUI(folder, 'Address to load', item.address).onChange(e => { 70 71 items[i].address = e; 71 72 });
+9 -15
features/slides/utils.js
··· 1 1 const id = 'features/utils'; 2 2 3 - const log = (...args) => { 4 - if (!window.app.debug) { 5 - return; 6 - } 7 - 8 - const aargs = [...args]; 9 - const source = aargs.shift(); 10 - const str = aargs.map(JSON.stringify).join(', '); 11 - //const str = aargs.join(', '); 12 - console.log(str); 13 - window.app.log(source, str); 14 - }; 15 - 16 3 const openStore = (prefix, defaults, clear = false) => { 17 4 18 5 //log(id, 'openStore', prefix, (defaults ? Object.keys(defaults) : '')); ··· 88 75 return ctr; 89 76 } 90 77 78 + const flattenObj = o => Object.keys(o).map(k => `${k}=${o[k]}`).join(','); 79 + 80 + const openWindow = (address, params) => { 81 + const target = params.hasOwnProperty('key') ? params.key : '_blank'; 82 + return window.open(address, target, flattenObj(params)); 83 + }; 84 + 91 85 export { 92 - log, 93 86 openStore, 94 - addToGUI 87 + addToGUI, 88 + openWindow 95 89 };
+151
features/test/app.js
··· 1 + console.log('test'); 2 + 3 + /* 4 + 5 + - windows 6 + - open window / is open 7 + - close window / is closed 8 + - hide window / is hidden 9 + - show window / is visible 10 + - move window / is moved 11 + - open hidden window 12 + - open web child window / is open 13 + - close web child window is closed 14 + - test ESC to close 15 + - test blur to close (and optional) 16 + - target singleton 17 + 18 + - multiple windows 19 + - commands routed to correct window 20 + 21 + - test window names 22 + */ 23 + 24 + const api = window.app; 25 + const debug = api.debug; 26 + const clear = false; 27 + 28 + // maps app id to BrowserWindow id (background) 29 + const windows = new Map(); 30 + 31 + const testShortcut = () => { 32 + const shortcut = 'Opt+0'; 33 + api.shortcuts.register(shortcut, () => { 34 + console.log('shortcut executed'); 35 + api.shortcuts.unregister(shortcut, () => { 36 + console.log('shortcut unregistered'); 37 + }); 38 + console.log('shortcut registered'); 39 + }); 40 + }; 41 + 42 + const testOpenCloseWindow = f => { 43 + console.log('test open/close window'); 44 + 45 + const url = 'http://localhost/'; 46 + const target = `prefix:${Date.now()}`; 47 + const params = {}; 48 + 49 + const w = window.open(url, target, {}); 50 + console.log('window is closed', w.closed); 51 + 52 + /* 53 + w.onload = () => { console.log('onload'); }; 54 + w.onclose = () => { console.log('onclose'); }; 55 + */ 56 + 57 + //api.window.close('_self'); 58 + api.window.close(target); 59 + console.log('window is closed', w.closed); 60 + 61 + /* 62 + // test hide/show window 63 + api.modifyWindow(target, { 64 + hide: true 65 + }); 66 + */ 67 + 68 + /* 69 + setTimeout(() => { 70 + w.close(); 71 + }, 1000); 72 + */ 73 + 74 + /* 75 + setTimeout(() => { 76 + window.app.closeWindow(target, r => { 77 + console.log('closeWindow() resp', r); 78 + }); 79 + }, 3000); 80 + */ 81 + 82 + }; 83 + 84 + const testPubSub = () => { 85 + console.log('test pubsub'); 86 + 87 + const topic = 'foo'; 88 + const msg = { value: 'bar' }; 89 + 90 + // Listen for system- or feature-level requests to open windows. 91 + window.app.subscribe(topic, msg => { 92 + console.log('received', msg.value === msg.value); 93 + }); 94 + 95 + // main process uses these for initialization 96 + window.app.publish(topic, msg, window.app.scopes.SYSTEM); 97 + console.log('published'); 98 + }; 99 + 100 + // unused, worth testing more tho 101 + const initIframeFeature = file => { 102 + const pathPrefix = 'file:///Users/dietrich/misc/peek/'; 103 + console.log('initiframe'); 104 + const i = document.createElement('iframe'); 105 + const src = pathPrefix + file; 106 + console.log('iframe src', src); 107 + document.body.appendChild(i); 108 + i.src = src; 109 + console.log('iframe inited'); 110 + i.addEventListener('load', () => { 111 + console.log('iframe loaded'); 112 + }); 113 + }; 114 + 115 + const init = () => { 116 + console.log('init'); 117 + 118 + testOpenCloseWindow(); 119 + }; 120 + 121 + window.addEventListener('load', init); 122 + 123 + /* 124 + const odiff = (a, b) => Object.entries(b).reduce((c, [k, v]) => Object.assign(c, a[k] ? {} : { [k]: v }), {}); 125 + 126 + const onStorageChange = (e) => { 127 + const old = JSON.parse(e.oldValue); 128 + const now = JSON.parse(e.newValue); 129 + 130 + const featureKey = `${id}+${storageKeys.FEATURES}`; 131 + //console.log('onStorageChane', e.key, featureKey) 132 + if (e.key == featureKey) { 133 + //console.log('STORAGE CHANGE', e.key, old[0].enabled, now[0].enabled); 134 + features().forEach((feat, i) => { 135 + console.log(feat.title, i, feat.enabled, old[i].enabled, now[i].enabled); 136 + // disabled, so unload 137 + if (old[i].enabled == true && now[i].enabled == false) { 138 + // TODO 139 + console.log('TODO: add unloading of features', feat) 140 + } 141 + // enabled, so load 142 + else if (old[i].enabled == false && now[i].enabled == true) { 143 + initFeature(feat); 144 + } 145 + }); 146 + } 147 + //JSON.stringify(e.storageArea); 148 + }; 149 + 150 + window.addEventListener('storage', onStorageChange); 151 + */
+12
features/test/index.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> 6 + <meta http-equiv="Content-Security-Policy" content="script-src 'self';"> 7 + <title>peek:test:index</title> 8 + </head> 9 + <body> 10 + <script type=module src="./app.js"></script> 11 + </body> 12 + </html>
+89
features/test/utils.js
··· 1 + const id = 'features/utils'; 2 + 3 + const openStore = (prefix, defaults, clear = false) => { 4 + 5 + //console.log(id, 'openStore', prefix, (defaults ? Object.keys(defaults) : '')); 6 + 7 + // multiple contexts 8 + const keyify = k => `${prefix}+${k}`; 9 + 10 + // Simple localStorage abstraction/wrapper 11 + const store = { 12 + set: (k, v) => { 13 + const key = keyify(k); 14 + const value = JSON.stringify(v); 15 + //console.log(id, 'store.set', key) 16 + localStorage.setItem(key, value); 17 + }, 18 + get: (k) => { 19 + const key = keyify(k); 20 + //console.log(id, 'store.get', key) 21 + const r = localStorage.getItem(key); 22 + return r ? JSON.parse(r) : null; 23 + }, 24 + clear: () => localStorage.clear() 25 + }; 26 + 27 + if (window.app.debug 28 + && window.app.debugLevel == window.app.debugLevels.FIRST_RUN) { 29 + //console.log(id, 'openStore(): clearing storage') 30 + store.clear(); 31 + } 32 + 33 + if (clear) { 34 + store.clear(); 35 + } 36 + 37 + const initStore = (store, data) => { 38 + Object.keys(data).forEach(k => { 39 + const v = store.get(k); 40 + if (!v) { 41 + //console.log(id, 'openStore(): init is setting', k, data[k]); 42 + store.set(k, data[k]); 43 + } 44 + }); 45 + }; 46 + 47 + if (defaults != null) { 48 + //console.log('UTILS/openStore()', 'initing'); 49 + initStore(store, defaults); 50 + } 51 + 52 + return store; 53 + }; 54 + 55 + const addToGUI = (gui, label, value, disabled = false, step = null, max = null) => { 56 + const params = {}; 57 + params[label] = value; 58 + 59 + const ctr = gui.add(params, label); 60 + 61 + /* 62 + if (disabled == true) { 63 + ctr.disable(); 64 + } 65 + 66 + if (max != null) { 67 + ctr.max(max); 68 + } 69 + 70 + if (step != null) { 71 + ctr.step(step); 72 + } 73 + */ 74 + 75 + return ctr; 76 + } 77 + 78 + const flattenObj = o => Object.keys(o).map(k => `${k}=${o[k]}`).join(','); 79 + 80 + const openWindow = (address, params) => { 81 + const target = params.hasOwnProperty('key') ? params.key : '_blank'; 82 + return window.open(address, target, flattenObj(params)); 83 + }; 84 + 85 + export { 86 + openStore, 87 + addToGUI, 88 + openWindow 89 + };
+4 -15
features/utils.js
··· 1 1 const id = 'features/utils'; 2 2 3 - const log = (...args) => { 4 - if (!window.app.debug) { 5 - return; 6 - } 7 - 8 - const aargs = [...args]; 9 - const source = aargs.shift(); 10 - const str = aargs.map(JSON.stringify).join(', '); 11 - //const str = aargs.join(', '); 12 - console.log(str); 13 - window.app.log(source, str); 14 - }; 15 - 16 3 const openStore = (prefix, defaults, clear = false) => { 17 4 18 5 //log(id, 'openStore', prefix, (defaults ? Object.keys(defaults) : '')); ··· 88 75 return ctr; 89 76 } 90 77 78 + const flattenObj = o => Object.keys(o).map(k => `${k}=${o[k]}`).join(','); 79 + 91 80 export { 92 - log, 93 81 openStore, 94 - addToGUI 82 + addToGUI, 83 + flattenObj 95 84 };
+421 -271
index.js
··· 1 1 // main.js 2 - (async () => { 3 2 4 - console.log('main'); 5 - 6 - const DEBUG = process.env.DEBUG; 7 - const DEBUG_LEVELS = { 8 - BASIC: 1, 9 - FIRST_RUN: 2 10 - }; 11 - const DEBUG_LEVEL = DEBUG_LEVELS.BASIC; 12 - //const DEBUG_LEVEL = DEBUG_LEVELS.FIRST_RUN; 13 - 14 - // Modules to control application life and create native browser window 15 3 const { 16 4 electron, 17 5 app, ··· 29 17 const path = require('node:path'); 30 18 const { pathToFileURL } = require('url'); 31 19 20 + (async () => { 21 + 22 + console.log('main'); 23 + 24 + const DEBUG = process.env.DEBUG || false; 25 + const DEBUG_LEVELS = { 26 + BASIC: 1, 27 + FIRST_RUN: 2 28 + }; 29 + const DEBUG_LEVEL = DEBUG_LEVELS.BASIC; 30 + //const DEBUG_LEVEL = DEBUG_LEVELS.FIRST_RUN; 31 + 32 32 // script loaded into every app window 33 33 const preloadPath = path.join(__dirname, 'preload.js'); 34 34 35 35 const APP_SCHEME = 'peek'; 36 + const APP_PROTOCOL = `${APP_SCHEME}:`; 36 37 const APP_CORE_PATH = 'features'; 38 + 39 + const APP_DEF_WIDTH = 1024; 40 + const APP_DEF_HEIGHT = 768; 37 41 38 42 // app hidden window to load 39 43 // core application logic is here 40 - const webCoreAddress = 'peek://core/background.html'; 44 + //const webCoreAddress = 'peek://core/background.html'; 45 + const webCoreAddress = 'peek://test/index.html'; 46 + 47 + const systemAddress = 'peek://system/'; 48 + 49 + const strings = { 50 + defaults: { 51 + quitShortcut: 'Option+q' 52 + }, 53 + msgs: { 54 + registerShortcut: 'registershortcut', 55 + unregisterShortcut: 'unregistershortcut', 56 + publish: 'publish', 57 + subscribe: 'subscribe', 58 + closeWindow: 'closewindow', 59 + console: 'console', 60 + }, 61 + topics: { 62 + prefs: 'topic:core:prefs' 63 + }, 64 + shortcuts: { 65 + errorAlreadyRegistered: 'Shortcut already registered', 66 + errorRegistrationFailed: 'Shortcut registration failed' 67 + } 68 + }; 41 69 42 70 const p = process.env.PROFILE; 43 71 console.log('env prof?', p, p != undefined, typeof p) ··· 81 109 app.setPath('sessionData', sessionDataPath); 82 110 83 111 // ***** Developer / Error handling / Etc ***** 112 + 113 + /* 84 114 const isDev = require('electron-is-dev'); 85 115 86 116 if (isDev) { 87 - // Enable live reload for Electron too 88 - require('electron-reload')(__dirname, { 89 - // Note that the path to electron may vary according to the main file 90 - electron: require(`${__dirname}/node_modules/electron`) 91 - }); 92 - /* 93 - try { 94 - require('electron-reloader')(module); 95 - } catch {} 96 - */ 97 117 } 118 + */ 98 119 99 120 const unhandled = require('electron-unhandled'); 100 121 unhandled(); ··· 138 159 139 160 // ***** Caches ***** 140 161 141 - const windowCache = { 142 - cache: [], 143 - add: entry => windowCache.cache.push(entry), 144 - byId: id => windowCache.cache.find(w => w.id == id), 145 - byKey: key => windowCache.cache.find(w => w.key == key), 146 - hasKey: key => windowCache.byKey(key) != undefined, 147 - indexOfKey: key => windowCache.cache.findIndex(w => w.key == key), 148 - removeByKey: key => windowCache.cache.splice(windowCache.indexOfKey(key), 1) 149 - }; 162 + // keyed on window id 163 + const _windows = new Map(); 150 164 151 - const _shortcuts = {}; 165 + // keyed on source address 166 + const shortcuts = new Map(); 152 167 153 - const _prefs = {}; 168 + // app global prefs configurable by user 169 + // populated during app init 170 + let _prefs = {}; 154 171 155 172 // ***** pubsub ***** 156 173 174 + const getPseudoHost = str => str.split('/')[2]; 175 + 176 + const scopes = { 177 + SYSTEM: 1, 178 + SELF: 2, 179 + GLOBAL: 3 180 + }; 181 + 157 182 const pubsub = (() => { 158 183 159 184 const topics = new Map(); 160 185 186 + const scopeCheck = (pubSource, subSource, scope) => { 187 + //console.log('scopeCheck', subSource, pubSource, scope); 188 + if (subSource == systemAddress) { 189 + return true 190 + } 191 + if (scope == scopes.GLOBAL) { 192 + return true; 193 + } 194 + if (getPseudoHost(subSource) == getPseudoHost(pubSource)) { 195 + return true; 196 + } 197 + return false; 198 + }; 199 + 161 200 return { 162 - publish: (topic, msg) => { 163 - console.log('ps.pub', topic, msg); 201 + publish: (source, scope, topic, msg) => { 202 + console.log('ps.pub', topic); 203 + 164 204 if (topics.has(topic)) { 165 - topics.get(topic).forEach(subscriber => { 166 - subscriber(msg); 167 - }); 205 + 206 + const t = topics.get(topic); 207 + 208 + for (const [subSource, cb] of t) { 209 + if (scopeCheck(source, subSource, scope)) { 210 + //console.log('FOUND ONE!', subSource); 211 + cb(msg); 212 + } 213 + }; 168 214 } 169 215 }, 170 - subscribe: (topic, cb) => { 171 - console.log('ps.sub', topic); 216 + subscribe: (source, scope, topic, cb) => { 217 + console.log('ps.sub', source, scope, topic); 218 + 172 219 if (!topics.has(topic)) { 173 - topics.set(topic, [cb]); 220 + topics.set(topic, new Map([ [source, cb] ])); 174 221 } 175 222 else { 176 223 const subscribers = topics.get(topic); 177 - subscribers.push(cb); 224 + subscribers.set(source, cb); 178 225 topics.set(topic, subscribers); 179 226 } 180 227 }, ··· 195 242 _tray.setToolTip(labels.tray.tooltip); 196 243 _tray.on('click', () => { 197 244 pubsub.publish('open', { 198 - feature: _prefs['features/core'].startupFeature 245 + address: _prefs.startupFeature 199 246 }); 200 247 }); 201 248 } ··· 272 319 // handle peek:// 273 320 initAppProtocol(); 274 321 275 - // init web core 276 - const rootWin = openWindow({ 277 - feature: 'Core', 278 - address: webCoreAddress, 279 - show: true, 280 - keepLive: true, 281 - keepVisible: true, 282 - debug: DEBUG 283 - }) 322 + // listen for app prefs to configure ourself 323 + // TODO: kinda janky, needs rethink 324 + pubsub.subscribe(systemAddress, scopes.SYSTEM, strings.topics.prefs, msg => { 325 + console.log('PREFS', msg); 284 326 285 - pubsub.subscribe('prefs', msg => { 286 327 // cache all prefs 287 - _prefs[msg.feature] = msg.prefs; 328 + _prefs = msg.prefs; 288 329 289 330 // show/hide in dock and tab switcher 290 - if (app.dock && msg.prefs.showInDockAndSwitcher == false) { 331 + if (DEBUG == false || (app.dock && msg.prefs.showInDockAndSwitcher == false)) { 332 + console.log('hiding dock'); 291 333 app.dock.hide(); 292 334 } 293 335 294 336 // initialize system tray 295 337 if (msg.prefs.showTrayIcon == true) { 338 + console.log('showing tray'); 296 339 initTray(); 297 340 } 341 + }); 298 342 299 - // open default app 300 - pubsub.publish('open', { 301 - feature: msg.prefs.startupFeature 302 - }); 343 + // init web core 344 + const winPrefs = { 345 + show: false, //DEBUG, 346 + webPreferences: { 347 + preload: preloadPath, 348 + webSecurity: false 349 + } 350 + }; 351 + 352 + const win = new BrowserWindow(winPrefs); 353 + win.loadURL(webCoreAddress); 354 + 355 + winDevtoolsConfig(win); 356 + 357 + win.webContents.setWindowOpenHandler(d => { 358 + //console.log('CORE BG WINOPENHANDLER', d); 359 + return winOpenHandler(webCoreAddress, d); 303 360 }); 304 361 305 - // eh, for helpers really 306 - registerShortcut('Option+q', onQuit); 362 + // TODO: this should be pref'd 363 + registerShortcut(strings.defaults.quitShortcut, onQuit); 307 364 }; 308 365 309 366 app.whenReady().then(onReady); 310 367 311 368 // ***** API ***** 312 369 313 - ipcMain.on('registershortcut', (ev, msg) => { 314 - //_shortcuts[msg.shortcut] = msg.replyTopic; 370 + ipcMain.on(strings.msgs.registerShortcut, (ev, msg) => { 371 + console.log('ipc register shortcut', msg); 372 + 373 + // record source of shortcut 374 + shortcuts.set(msg.shortcut, msg.source); 375 + 315 376 registerShortcut(msg.shortcut, () => { 316 377 console.log('on(registershortcut): shortcut executed', msg.shortcut, msg.replyTopic) 317 - ev.reply(msg.replyTopic, {}); 378 + ev.reply(msg.replyTopic, { foo: 'bar' }); 318 379 }); 319 380 }); 320 381 321 - ipcMain.on('unregistershortcut', (ev, msg) => { 322 - if (globalShortcut.isRegistered(msg.shortcut)) { 323 - globalShortcut.unregister(msg.shortcut); 324 - } 325 - }); 382 + ipcMain.on(strings.msgs.unregisterShortcut, (ev, msg) => { 383 + console.log('ipc unregister shortcut', msg); 326 384 327 - ipcMain.on('openwindow', (ev, msg) => { 328 - openWindow(msg.params, output => { 329 - if (msg && msg.replyTopic) { 330 - ev.reply(msg.replyTopic, output); 331 - } 385 + unregisterShortcut(msg.shortcut, res => { 386 + console.log('ipc unregister shortcut callback result:', res); 332 387 }); 333 388 }); 334 389 335 - ipcMain.on('closewindow', (ev, msg) => { 390 + ipcMain.on(strings.msgs.closeWindow, (ev, msg) => { 336 391 closeWindow(msg.params, output => { 392 + console.log('main.closeWindow api callback, output:', output); 337 393 if (msg && msg.replyTopic) { 338 394 ev.reply(msg.replyTopic, output); 339 395 } ··· 341 397 }); 342 398 343 399 // generic dispatch - messages only from trusted code (💀) 344 - ipcMain.on('publish', (ev, msg) => { 400 + ipcMain.on(strings.msgs.publish, (ev, msg) => { 345 401 console.log('ipc:publish', msg); 346 402 347 - pubsub.publish(msg.topic, msg.data); 403 + pubsub.publish(msg.source, msg.scope, msg.topic, msg.data); 348 404 }); 349 405 350 - ipcMain.on('subscribe', (ev, msg) => { 406 + ipcMain.on(strings.msgs.subscribe, (ev, msg) => { 351 407 console.log('ipc:subscribe', msg); 352 408 353 - pubsub.subscribe(msg.topic, data => { 409 + pubsub.subscribe(msg.source, msg.scope, msg.topic, data => { 354 410 console.log('ipc:subscribe:notification', msg); 355 411 ev.reply(msg.replyTopic, data); 356 412 }); 357 413 }); 358 414 359 - // ipc ESC handler 360 - // close focused window on Escape 361 - ipcMain.on('esc', (ev, title) => { 362 - console.log('index.js: ESC'); 363 - // XXX remove 364 - return; 415 + ipcMain.on(strings.msgs.console, (ev, msg) => { 416 + console.log('r:', msg.source, msg.text); 417 + }); 418 + 419 + ipcMain.on('modifywindow', (ev, msg) => { 420 + console.log('modifywindow', msg); 365 421 366 - const fwin = BrowserWindow.getFocusedWindow(); 367 - const entry = windowCache.byId(fwin.id); 368 - // focused window is managed by me 369 - // so hide it instead of actually closing it 370 - if (entry) { 371 - BrowserWindow.fromId(entry.id).hide(); 372 - console.log('index.js: ESC: hiding focused content window'); 422 + const key = msg.hasOwnProperty('name') ? msg.name : null; 423 + 424 + if (key != null) { 425 + for (const [id, w] of _windows) { 426 + console.log('win?', w.source, msg.source, w.params.key, key); 427 + if (w.source == msg.source && w.params.key == key) { 428 + console.log('FOUND WINDOW FOR KEY', key); 429 + const bw = BrowserWindow.fromId(id); 430 + let r = false; 431 + try { 432 + modWindow(bw, msg.params); 433 + r = true; 434 + } 435 + catch(ex) { 436 + console.error(ex); 437 + } 438 + ev.reply(msg.replyTopic, { output: r }); 439 + } 440 + } 373 441 } 374 - // focused window is me 375 - else if (!fwin.isDestroyed()) { 376 - fwin.close(); 377 - console.log('index.js: ESC: closing focused window, is not in cache and not destroyed'); 378 - } 442 + 379 443 }); 380 444 381 - ipcMain.on('console', (ev, msg) => { 382 - console.log('r:', msg.source, msg.text); 383 - }); 445 + const modWindow = (bw, params) => { 446 + if (params.action == 'close') { 447 + bw.close(); 448 + } 449 + if (params.action == 'hide') { 450 + bw.hide(); 451 + } 452 + if (params.action == 'show') { 453 + bw.show(); 454 + } 455 + }; 384 456 385 457 // ***** Helpers ***** 386 458 ··· 388 460 console.log('registerShortcut', shortcut) 389 461 390 462 if (globalShortcut.isRegistered(shortcut)) { 463 + console.error(strings.shortcuts.errorAlreadyRegistered, shortcut); 391 464 globalShortcut.unregister(shortcut); 465 + return new Error(strings.shortcuts.errorAlreadyRegisterd); 392 466 } 393 467 394 468 const ret = globalShortcut.register(shortcut, () => { ··· 396 470 callback(); 397 471 }); 398 472 399 - if (!ret) { 473 + if (ret != true) { 400 474 console.error('Unable to register shortcut', shortcut); 401 - return new Error("Failed in some way", { cause: err }); 475 + return new Error(strings.shortcuts.errorRegistrationFailed); 402 476 } 403 477 }; 404 478 405 - // window opener 406 - const openWindow = (params, callback) => { 407 - console.log('openWindow', params, callback != null); 479 + const unregisterShortcut = (shortcut, callback) => { 480 + console.log('unregisterShortcut', shortcut) 408 481 409 - // if no source identifier, barf 410 - // TODO: test the protocol 411 - if (!params.hasOwnProperty('feature') || params.feature == undefined) { 412 - throw new Error('openWindow: no identifying source for openWindow request!'); 482 + if (!globalShortcut.isRegistered(shortcut)) { 483 + console.error('Unable to unregister shortcut because not registered or it is not us', shortcut); 484 + return new Error("Failed in some way", { cause: err }); 413 485 } 414 486 415 - // TODO: need to figure out a better approach 416 - const show = params.hasOwnProperty('show') ? params.show : true; 487 + globalShortcut.unregister(shortcut, () => { 488 + console.log('shortcut unregistered', shortcut); 417 489 418 - // keep visible 419 - const keepVisible = params.hasOwnProperty('keepVisible') ? params.keepVisible : false; 490 + // delete from cache 491 + shortcuts.delete(shortcut); 492 + callback(); 493 + }); 494 + }; 420 495 421 - // validate address 422 - if (!params.hasOwnProperty('address') || params.address.length <= 0) { 423 - console.error('openWindow: no address or is empty!'); 424 - return; 496 + // unregister any shortcuts this address registered 497 + // and delete entry from cache 498 + const unregisterShortcutsForAddress = (aAddress) => { 499 + for (const [shortcut, address] of shortcuts) { 500 + if (address == aAddress) { 501 + console.log('unregistering', shortcut, 'for', address); 502 + unregisterShortcut(shortcut); 503 + } 425 504 } 505 + }; 426 506 427 - // cache key 428 - // window keys can be provided by features. 429 - // eg for different slides that have same url, don't want to re-use window. 430 - // 431 - // otherwise use a simple concat 432 - // 433 - // TODO: need to figure out a better approach 434 - const key = params.key ? params.key : (params.feature + (params.address || params.file)); 435 - console.log('openWindow', 'cache key', key); 507 + // esc handler 508 + // TODO: make user-configurable 509 + const addEscHandler = bw => { 510 + console.log('adding esc handler'); 511 + bw.webContents.on('before-input-event', (e, i) => { 512 + //console.log('BIE', i.type, i.key); 513 + if (i.key == 'Escape' && i.type == 'keyUp') { 514 + console.log('webcontents.onBeforeInputEvent(): esc'); 515 + closeOrHideWindow(bw.id); 516 + } 517 + }); 518 + }; 436 519 437 - if (windowCache.hasKey(key)) { 438 - console.log('REUSING WINDOW for ', key) 439 - const entry = windowCache.byKey(key); 440 - if (entry != undefined) { 441 - const win = BrowserWindow.fromId(entry.id); 442 - if (win) { 443 - console.log('openWindow: opening persistent window for', key) 444 - if (show) { 445 - win.show(); 446 - } 447 - else { 448 - // asking to open an already cached window 449 - // eg background app processes that weren't cleaned up maybe? 450 - } 520 + // configure windows opened by renderers 521 + const winOpenHandler = (source, details) => { 522 + console.log('WINOPENHANDLER', source, details); 451 523 452 - if (callback != null) { 453 - callback({ 454 - cache: true, 455 - key: key 456 - }); 457 - } 524 + /* 525 + // TODO: do something that allows popping out 526 + // into default browser 527 + if (details.url.startsWith('http')) { 528 + shell.openExternal(details.url); 529 + return { action: 'deny' }; 530 + } 531 + */ 532 + 533 + const params = {}; 534 + details.features.split(',') 535 + .map(entry => entry.split('=')) 536 + // TODO: ugh 537 + .map(entry => { 538 + entry[1] = (entry[1] === 'false') ? false : true; 539 + return entry; 540 + }) 541 + .forEach(entry => params[entry[0]] = entry[1]); 542 + 543 + console.log('params', params); 544 + 545 + const overrides = { 546 + devTools: true, //DEBUG || params.debug, 547 + skipTaskbar: true, // TODO 548 + autoHideMenuBar: true, // TODO 549 + titleBarStyle: 'hidden', // TODO 550 + webPreferences: { 551 + preload: preloadPath 552 + } 553 + }; 554 + 555 + smash(params, overrides, 'show', null, true); 458 556 459 - return; 557 + // keys are used to force singleton windows 558 + // TODO: this doesn't really do anything rn 559 + const key = params.hasOwnProperty('key') ? params.key : null; 560 + if (key != null) { 561 + _windows.forEach((w) => { 562 + if (w.source == source && w.params.key == key) { 563 + console.log('WINDOW ALREADY EXISTS FOR KEY', key); 564 + //id = w.id; 460 565 } 461 - } 462 - } 463 - else { 464 - console.log('KEY NOT IN CACHE'); 566 + }); 465 567 } 466 568 467 - console.log('openWindow(): creating new window'); 569 + // TODO: unhack 570 + const onBrowserWinCreated = (e, bw) => { 571 + console.log('onBrowserWinCreated', bw.id); 572 + app.off('browser-window-created', onBrowserWinCreated); 468 573 469 - const height = params.height || 600; 470 - const width = params.width || 800; 574 + // Capture new content windows created from this content window 575 + // (not firing sometimes, wtf? maybe in debug/not mode?) 576 + bw.webContents.on('did-create-window', (w, d) => { 577 + console.log('DID-CREATE-WINDOW', w, d); 578 + }); 471 579 472 - let webPreferences = {}; 580 + const didFinishLoad = () => { 581 + console.log('DID-FINISH-LOAD()'); 473 582 474 - const url = new URL(params.address); 583 + // TODO: unhack 584 + const url = bw.webContents.getURL(); 475 585 476 - if (url.protocol == APP_SCHEME + ':') { 477 - console.log('APP ADDRESS', params.address); 586 + console.log('dFL(): url', url, details.url); 587 + console.log('dfl', bw.id); 478 588 479 - //params.address = `file://${path.join(__dirname)}/${params.file}`; 589 + if (url == details.url) { 590 + bw.webContents.off('did-finish-load', didFinishLoad); 480 591 481 - // add preload 482 - webPreferences.preload = preloadPath; 483 - } 592 + //params.address = url; 484 593 485 - if (!params.persistData) { 486 - // TODO: hack. this just isolates. 487 - webPreferences.partition = Date.now() 488 - } 594 + addEscHandler(bw); 489 595 490 - let winPreferences = { 491 - height, 492 - width, 493 - show, 494 - skipTaskbar: true, 495 - autoHideMenuBar: true, 496 - titleBarStyle: 'hidden', 497 - webPreferences 498 - }; 596 + winDevtoolsConfig(bw); 499 597 500 - ['x', 'y'].forEach( k => { 501 - if (params.hasOwnProperty(k)) { 502 - winPreferences[k] = params[k]; 503 - } 504 - }); 598 + // don't do this in detached debug mode, devtools steals focus 599 + // and closes everything 😐 600 + // TODO: fix w/ devtoolsIsFocused() 601 + // - enumerat windows 602 + // - find devtools 603 + // - if exists and has focus, then bail 604 + // TODO: should be opener-configurable param 605 + if (!DEBUG) { 606 + bw.on('blur', () => { 607 + console.log('dFL.onBlur() for', url); 608 + closeOrHideWindow(bw.id); 609 + }); 610 + } 611 + 612 + // post actual close clean-up 613 + bw.on('closed', () => { 614 + console.log('dFL.onClosed: deleting ', bw.id, ' for ', url); 505 615 506 - if (winPreferences.x == undefined && winPreferences.y == undefined) { 507 - winPreferences.center = true; 508 - } 616 + // unregister any shortcuts this window registered 617 + const isPrivileged = url.startsWith(APP_PROTOCOL); 618 + if (isPrivileged) { 619 + unregisterShortcutsForAddress(url) 620 + } 509 621 510 - console.log('final dimension params (x, y, center)', winPreferences.x, winPreferences.y, winPreferences.center); 622 + // remove from cache 623 + _windows.delete(bw.id); 511 624 512 - let win = new BrowserWindow(winPreferences); 625 + bw = null; 626 + }); 513 627 514 - // if persisting window, cache the caller's key and window id 515 - if (params.keepLive == true || DEBUG) { 516 - windowCache.add({ 517 - id: win.id, 518 - key, 519 - params 520 - }); 521 - } 628 + // add to cache 629 + _windows.set(bw.id, { 630 + id: bw.id, 631 + source, 632 + params 633 + }); 522 634 523 - // TODO: make configurable 524 - const onGoAway = () => { 525 - if (params.keepLive == true) { 526 - if (params.keepVisible == false) { 527 - console.log('main.onGoAway(): hiding ', params.address); 528 - win.hide(); 635 + /* 636 + // send synthetic msg to source, notifying window was opened 637 + pubsub.publish(source, scopes.SELF, 'onWindowOpened', { 638 + url, 639 + key 640 + }); 641 + */ 529 642 } 530 - // else keep window alive and visible! 643 + }; 644 + 645 + bw.webContents.on('did-finish-load', didFinishLoad); 646 + }; 647 + 648 + app.on('browser-window-created', onBrowserWinCreated); 649 + 650 + console.log('OVERRIDES', overrides); 651 + 652 + return { 653 + action: 'allow', 654 + overrideBrowserWindowOptions: overrides 655 + }; 656 + }; 657 + 658 + // show/configure devtools when/after a window is opened 659 + const winDevtoolsConfig = bw => { 660 + // TODO: make detach mode configurable 661 + // really want to get so individual app windows can easily control this 662 + // for themselves 663 + bw.webContents.openDevTools({ mode: 'detach' }); 664 + //win.webContents.openDevTools(); 665 + 666 + // when devtools completely open 667 + bw.webContents.on('devtools-opened', () => { 668 + // if window is visible, focus content window 669 + if (bw.isVisible()) { 670 + bw.webContents.focus(); 531 671 } 672 + // otherwise force devtools focus 673 + // (for some reason doesn't focus when no visible window...) 532 674 else { 533 - console.log('win.onGoAway(): destroying ', params.address); 534 - win.destroy(); 675 + app.focus(); 535 676 } 536 - } 677 + }); 678 + }; 537 679 538 - // don't do this in detached debug mode, devtools steals focus 539 - // and closes everything 😐 540 - // TODO: fix 541 - // TODO: should be configurable behavior 542 - win.on('blur', onGoAway); 543 - 544 - win.on('close', onGoAway); 680 + // window closer 681 + // this will actually close the the window 682 + // regardless of "keep alive" opener params 683 + const closeWindow = (params, callback) => { 684 + console.log('closeWindow', params, callback != null); 685 + 686 + let retval = false; 687 + 688 + if (params.hasOwnProperty('id') && _windows.has(params.id)) { 689 + console.log('closeWindow(): closing', params.id); 690 + 691 + const entry = _windows.get(params.id); 692 + if (!entry) { 693 + // wtf 694 + return; 695 + } 545 696 546 - win.on('closed', () => { 547 - console.log('win.on(closed): deleting ', key, ' for ', params.address); 548 - windowCache.removeByKey(key); 549 - win = null; 550 - }); 697 + closeChildWindows(entry.params.address); 551 698 552 - //if (params.debug) { 553 - // TODO: why not working for core background page? 554 - //win.webContents.openDevTools({ mode: 'detach' }); 555 - win.webContents.openDevTools(); 556 - //} 699 + BrowserWindow.fromId(params.id).close(); 557 700 558 - if (params.address) { 559 - win.loadURL(params.address); 701 + retval = true; 560 702 } 561 - else { 562 - console.error('openWindow: neither address nor file!'); 703 + 704 + if (callback != null) { 705 + callback(retval); 563 706 } 707 + }; 564 708 565 - /* 566 - win.webContents.on('keyup', async () => { 567 - console.log('main: keyup') 568 - }); 569 - */ 709 + const closeOrHideWindow = id => { 710 + console.log('CLOSEORHIDEWINDOW', id); 570 711 571 - //const escScript = "window.addEventListener('keyup', e => window.close())"; 572 - //win.webContents.executeJavaScript(escScript); 712 + const win = BrowserWindow.fromId(id); 713 + if (win.isDestroyed()) { 714 + console.log('window already dead'); 715 + return; 716 + } 573 717 574 - //win.webContents.send('window', { type: labels.featureType, id: win.id}); 575 - //broadcastToWindows('window', { type: labels.featureType, id: win.id}); 718 + const entry = _windows.get(id); 576 719 577 - // TODO: fix func-level callback handling and resp obj 720 + if (!entry) { 721 + console.log('window not in cache, so closing (FIXME: should be in cache?)'); 722 + win.close(); 723 + return; 724 + } 578 725 579 - if (params.script) { 580 - const script = params.script; 581 - const domEvent = script.domEvent || 'dom-ready'; 726 + const params = entry.params; 582 727 583 - win.webContents.on(domEvent, async () => { 584 - try { 585 - const r = await win.webContents.executeJavaScript(script.script); 586 - if (callback) { 587 - callback({ 588 - key: key, 589 - scriptOutput: r 590 - }); 591 - } 592 - } catch(ex) { 593 - console.error('cs exec error', ex); 594 - } 595 - if (script.closeOnCompletion) { 596 - win.destroy(); 597 - } 598 - }); 728 + if (params.keepLive == true) { 729 + console.log('closeOrHideWindow(): hiding ', params.address); 730 + win.hide(); 599 731 } 600 - else if (callback != null) { 601 - callback({ 602 - key: key 603 - }); 604 - } 732 + else { 733 + // close any open windows this window opened 734 + // TODO: need a "force" mode for this 735 + closeChildWindows(params.address); 605 736 606 - return win; 737 + console.log('closeOrHideWindow(): closing ', params.address); 738 + win.close(); 739 + } 740 + console.log('DONE closeorhidewindow'); 607 741 }; 608 742 609 - // window closer 610 - const closeWindow = (params, callback) => { 611 - console.log('closeWindow', params, callback != null); 743 + const closeChildWindows = (aAddress) => { 744 + console.log('closeChildWindows()', aAddress); 612 745 613 - if (windowCache.hasKey(params.key)) { 614 - const winData = windowCache.byKey(params.key); 615 - BrowserWindow.fromId(winData.id).close(); 746 + if (aAddress == webCoreAddress) { 747 + return; 616 748 } 617 - else { 618 - // wtf 619 - } 749 + 750 + for (const [id, entry] of _windows) { 751 + if (entry.source == aAddress) { 752 + const address = entry.params.address; 753 + console.log('closing child window', address, 'for', aAddress); 754 + 755 + // recurseme 756 + closeChildWindows(address); 620 757 621 - if (callback != null) { 622 - callback(); 758 + // close window 759 + BrowserWindow.fromId(id).close(); 760 + } 623 761 } 624 762 }; 625 763 ··· 647 785 // Close all persisent windows? 648 786 649 787 app.quit(); 788 + }; 789 + 790 + const smash = (source, target, k, d, noset = false) => { 791 + if (source.hasOwnProperty(k)) { 792 + target[k] = source[k]; 793 + } 794 + else if (noset) { 795 + /* no op */ 796 + } 797 + else { 798 + target[k] = d; 799 + } 650 800 }; 651 801 652 802 })();
+2 -4
package.json
··· 17 17 "@electron-forge/maker-zip": "7.2.0", 18 18 "electron": "27.1.3", 19 19 "electron-is-dev": "2.0.0", 20 - "electron-rebuild": "3.2.9", 21 - "electron-reload": "2.0.0-alpha.1", 22 - "electron-reloader": "1.2.3" 20 + "electron-rebuild": "3.2.9" 23 21 }, 24 22 "dependencies": { 25 23 "electron-squirrel-startup": "1.0.0", ··· 30 28 }, 31 29 "scripts": { 32 30 "start": "electron-forge start", 33 - "debug": "DEBUG=1 electron-forge start", 31 + "debug": "nodemon --exec DEBUG=1 electron-forge start", 34 32 "package": "electron-forge package", 35 33 "make": "electron-forge make" 36 34 }
+118 -71
preload.js
··· 3 3 ipcRenderer 4 4 } = require('electron'); 5 5 6 + 6 7 const src = 'preload'; 8 + console.log(src, 'init', window); 7 9 8 - const DEBUG = process.env.DEBUG; 10 + const DEBUG = process.env.DEBUG || false; 9 11 const DEBUG_LEVELS = { 10 12 BASIC: 1, 11 13 FIRST_RUN: 2 ··· 14 16 const DEBUG_LEVEL = DEBUG_LEVELS.BASIC; 15 17 //const DEBUG_LEVEL = DEBUG_LEVELS.FIRST_RUN; 16 18 17 - const log = (source, text) => { 18 - ipcRenderer.send('console', { 19 - source, 20 - text 21 - }); 22 - }; 19 + const APP_SCHEME = 'peek'; 20 + const APP_PROTOCOL = `${APP_SCHEME}:`; 23 21 24 - // all visible window types close on escape 25 - window.addEventListener('keyup', e => { 26 - log('preload', 'keyup', e.key, window.location) 27 - if (e.key == 'Escape') { 28 - ipcRenderer.send('esc', ''); 29 - } 30 - }); 22 + const sourceAddress = window.location.toString(); 31 23 32 - const addEscListener = () => { 33 - window.addEventListener('keyup', e => { 34 - log('preload', 'keyup', e.key, window.location) 35 - if (e.key == 'Escape') { 36 - ipcRenderer.send('esc', ''); 37 - } 38 - }); 39 - }; 24 + const rndm = () => Math.random().toString(16).slice(2); 40 25 41 26 let api = {}; 42 27 28 + api.debug = DEBUG; 29 + api.debugLevels = DEBUG_LEVELS; 30 + api.debugLevel = DEBUG_LEVEL; 31 + 43 32 api.shortcuts = { 44 33 register: (shortcut, cb) => { 45 - log(src, 'registering ' + shortcut + ' for ' + window.location) 34 + console.log(src, 'registering ' + shortcut + ' for ' + window.location) 46 35 47 - const replyTopic = `${shortcut}${window.location}`; 36 + //const replyTopic = `${shortcut}:${window.location}`; 37 + const replyTopic = `${shortcut}${rndm()}`; 48 38 49 39 ipcRenderer.send('registershortcut', { 40 + source: sourceAddress, 50 41 shortcut, 51 42 replyTopic 52 43 }); 53 44 54 45 ipcRenderer.on(replyTopic, (ev, msg) => { 55 - log(src, 'shortcut execution reply'); 46 + console.log(src, 'shortcut execution reply'); 56 47 cb(); 57 48 }); 58 49 }, 59 50 unregister: shortcut => { 60 51 console.log('unregistering', shortcut, 'for', window.location) 61 52 ipcRenderer.send('registershortcut', { 53 + source: sourceAddress, 62 54 shortcut 63 55 }); 64 56 } 65 57 }; 66 58 67 - api.openWindow = (params, callback) => { 68 - log(src, ['api.openwindow', JSON.stringify(params), 'for', window.location].join(', ')); 59 + api.closeWindow = (id, callback) => { 60 + console.log(src, ['api.closewindow', id, 'for', window.location].join(', ')); 61 + 62 + const replyTopic = `${id}${rndm()}`; 69 63 70 - // TODO: won't work for features that open multiple windows 71 - const replyTopic = `${params.feature}${params.address}`; 64 + const params = { 65 + source: sourceAddress, 66 + id 67 + }; 72 68 73 - ipcRenderer.send('openwindow', { 69 + ipcRenderer.send('closewindow', { 74 70 params, 75 71 replyTopic 76 72 }); 77 73 78 74 ipcRenderer.once(replyTopic, (ev, msg) => { 79 - log(src, 'api.openwindow', 'resp from main', msg); 75 + console.log(src, 'api.closewindow', 'resp from main', msg); 80 76 if (callback) { 81 77 callback(msg); 82 78 } 83 79 }); 84 80 }; 85 81 86 - api.closeWindow = (key, callback) => { 87 - log(src, ['api.closewindow', key, 'for', window.location].join(', ')); 82 + api.scopes = { 83 + SYSTEM: 1, 84 + SELF: 2, 85 + GLOBAL: 3 86 + }; 87 + 88 + api.publish = (topic, msg, scope = api.scopes.SELF) => { 89 + console.log(sourceAddress, 'publish', topic) 90 + 91 + // TODO: c'mon 92 + if (!topic) { 93 + return new Error('wtf'); 94 + } 95 + 96 + ipcRenderer.send('publish', { 97 + source: sourceAddress, 98 + scope, 99 + topic, 100 + data: msg, 101 + }); 102 + }; 103 + 104 + api.subscribe = (topic, callback, scope = api.scopes.SELF) => { 105 + console.log(src, 'subscribe', topic) 106 + 107 + // TODO: c'mon 108 + if (!topic || !callback) { 109 + return new Error('wtf'); 110 + } 88 111 89 - const replyTopic = `${key}${Math.random().toString(16).slice(2)}`; 112 + const replyTopic = `${topic}:${rndm()}`; 90 113 91 - ipcRenderer.send('closewindow', { 92 - params: { key }, 114 + ipcRenderer.send('subscribe', { 115 + source: sourceAddress, 116 + scope, 117 + topic, 93 118 replyTopic 94 119 }); 95 120 96 - ipcRenderer.once(replyTopic, (ev, msg) => { 97 - log(src, 'api.closewindow', 'resp from main', msg); 98 - if (callback) { 121 + ipcRenderer.on(replyTopic, (ev, msg) => { 122 + console.log('topic', topic, msg); 123 + msg.source = sourceAddress; 124 + try { 99 125 callback(msg); 126 + } 127 + catch(ex) { 128 + console.log('preload:subscribe subscriber callback errored for topic', topic, 'and source', sourceAddress, ex); 100 129 } 101 130 }); 102 131 }; 103 132 104 - api.log = log; 105 - api.debug = DEBUG; 106 - api.debugLevels = DEBUG_LEVELS; 107 - api.debugLevel = DEBUG_LEVEL; 133 + api.window = { 134 + close: target => { 135 + console.log('window.close', target); 108 136 109 - api.publish = (topic, msg) => { 110 - //log(src, 'publish', topic) 137 + if (target === null) { 138 + window.close(); 139 + return; 140 + } 111 141 112 - // noop if not an internal app file 113 - // TODO: hmmm 114 - const isMain = window.location.protocol == 'file:'; 115 - if (!isMain) { 116 - return; 142 + ipcRenderer.send('modifywindow', { 143 + source: sourceAddress, 144 + params: { 145 + action: 'close' 146 + } 147 + }); 148 + }, 149 + hide: () => { 150 + }, 151 + move: () => { 152 + }, 153 + show: () => { 117 154 } 155 + }; 118 156 119 - ipcRenderer.send('publish', { 120 - topic, 121 - data: msg, 157 + api.modifyWindow = (winName, params) => { 158 + console.log('modifyWindow(): window', winName, params); 159 + //w.name = `${sourceAddress}:${rndm()}`; 160 + console.log('NAME', winName); 161 + ipcRenderer.send('modifywindow', { 162 + source: sourceAddress, 163 + name: winName, 164 + params 122 165 }); 123 166 }; 124 167 125 - api.subscribe = (topic, callback) => { 126 - //log(src, 'subscribe', topic) 168 + // unused 169 + /* 170 + api.sendToWindow = (windowId, msg) => { 171 + ipcRenderer.send('sendToWindow', { 172 + source: sourceAddress, 173 + id, 174 + msg 175 + }); 176 + }; 127 177 128 - // noop if not an internal app file 129 - // TODO: hmmm 130 - const isMain = window.location.protocol == 'file:'; 131 - 132 - if (!isMain) { 133 - // TODO: error 134 - return; 178 + api.onMessage = callback => { 179 + // TODO: c'mon 180 + if (!topic || !callback) { 181 + return new Error('wtf'); 135 182 } 136 183 137 - const replyTopic = `${topic}${Math.random().toString(16).slice(2)}`; 184 + const replyTopic = `${topic}:${rndm()}`; 138 185 139 186 ipcRenderer.send('subscribe', { 187 + source: sourceAddress, 140 188 topic, 141 189 replyTopic 142 190 }); 143 191 144 192 ipcRenderer.on(replyTopic, (ev, msg) => { 145 - if (callback) { 146 - callback(msg); 147 - } 193 + msg.source = sourceAddress; 194 + callback(msg); 148 195 }); 149 196 }; 197 + */ 150 198 151 199 contextBridge.exposeInMainWorld('app', api); 200 + console.log(src, 'api exposed'); 152 201 153 - /* 154 202 window.addEventListener('load', () => { 155 - console.log('preload loaded'); 156 - log(src, 'preload loaded'); 203 + console.log(src, 'load', window); 157 204 }); 158 - */ 159 205 160 206 /* 161 207 const handleMainWindow = () => { 162 - d('handleMainWindow'); 163 208 window.addEventListener('load', () => { 164 209 const replaceText = (selector, text) => { 165 210 const element = document.getElementById(selector) ··· 173 218 }; 174 219 */ 175 220 221 + /* 176 222 window.addEventListener('DOMContentLoaded', () => { 177 223 const replaceText = (selector, text) => { 178 224 const element = document.getElementById(selector) ··· 183 229 replaceText(`${dependency}-version`, process.versions[dependency]) 184 230 } 185 231 }) 232 + */