pear-electron 1.5.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -814,6 +814,13 @@ Use `<script type="module" src="path/to/my-file.js">` to load a JavaScript Modul
814
814
 
815
815
  Use `<script src="path/to/my-file.js">` to load a JavaScript Script.
816
816
 
817
+
818
+ ## API Extension
819
+
820
+ ### `pear-electron/api`
821
+
822
+ Function that takes an API class and extends it with pear-electron APIs
823
+
817
824
  ## Graphical User Interface Options
818
825
 
819
826
  GUI options for an application are set in the application `package.json` `pear.gui` field.
package/api.js ADDED
@@ -0,0 +1,359 @@
1
+ /* globals Pear */
2
+ 'use strict'
3
+ const streamx = require('streamx')
4
+ const { EventEmitter } = require('events')
5
+
6
+ module.exports = (api) => {
7
+ class API extends api {
8
+ static UI = Symbol('ui')
9
+ #ipc = null
10
+ #pipe = null
11
+
12
+ constructor (ipc, state, teardown, id) {
13
+ super(ipc, state, { teardown })
14
+ this.#ipc = ipc
15
+ const kGuiCtrl = Symbol('gui:ctrl')
16
+ const media = {
17
+ status: {
18
+ microphone: () => ipc.getMediaAccessStatus({ id, media: 'microphone' }),
19
+ camera: () => ipc.getMediaAccessStatus({ id, media: 'camera' }),
20
+ screen: () => ipc.getMediaAccessStatus({ id, media: 'screen' })
21
+ },
22
+ access: {
23
+ microphone: () => ipc.askForMediaAccess({ id, media: 'microphone' }),
24
+ camera: () => ipc.askForMediaAccess({ id, media: 'camera' }),
25
+ screen: () => ipc.askForMediaAccess({ id, media: 'screen' })
26
+ },
27
+ desktopSources: (options = {}) => ipc.desktopSources(options)
28
+ }
29
+
30
+ class Found extends streamx.Readable {
31
+ #id = null
32
+ #stream = null
33
+ #listener = (data) => {
34
+ this.push(data.result)
35
+ }
36
+
37
+ constructor (id) {
38
+ super()
39
+ this.#id = id
40
+ this.#stream = ipc.found(this.#id)
41
+ this.#stream.on('data', this.#listener)
42
+ }
43
+
44
+ proceed () {
45
+ return ipc.find({ id: this.#id, next: true })
46
+ }
47
+
48
+ clear () {
49
+ if (this.destroyed) throw Error('Nothing to clear, already destroyed')
50
+ return ipc.find({ id: this.#id, stop: 'clear' }).finally(() => this.destroy())
51
+ }
52
+
53
+ keep () {
54
+ if (this.destroyed) throw Error('Nothing to keep, already destroyed')
55
+ return ipc.find({ id: this.#id, stop: 'keep' }).finally(() => this.destroy())
56
+ }
57
+
58
+ activate () {
59
+ if (this.destroyed) throw Error('Nothing to activate, already destroyed')
60
+ return ipc.find({ id: this.#id, stop: 'activate' }).finally(() => this.destroy())
61
+ }
62
+
63
+ _destroy () {
64
+ this.#stream.destroy()
65
+ return this.clear()
66
+ }
67
+ }
68
+
69
+ class Parent extends EventEmitter {
70
+ constructor (id) {
71
+ super()
72
+ this.id = id
73
+ ipc.receiveFrom(id, (...args) => {
74
+ this.emit('message', ...args)
75
+ })
76
+ }
77
+
78
+ async find (options) {
79
+ const found = new Found(this.id)
80
+ await ipc.find({ id: this.id, options })
81
+ return found
82
+ }
83
+
84
+ send (...args) { return ipc.sendTo(this.id, ...args) }
85
+ focus (options = null) { return ipc.parent({ act: 'focus', id: this.id, options }) }
86
+ blur () { return ipc.parent({ act: 'blur', id: this.id }) }
87
+ show () { return ipc.parent({ act: 'show', id: this.id }) }
88
+ hide () { return ipc.parent({ act: 'hide', id: this.id }) }
89
+ minimize () { return ipc.parent({ act: 'minimize', id: this.id }) }
90
+ maximize () { return ipc.parent({ act: 'maximize', id: this.id }) }
91
+ fullscreen () { return ipc.parent({ act: 'fullscreen', id: this.id }) }
92
+ restore () { return ipc.parent({ act: 'restore', id: this.id }) }
93
+ dimensions (options = null) { return ipc.parent({ act: 'dimensions', id: this.id, options }) }
94
+ isVisible () { return ipc.parent({ act: 'isVisible', id: this.id }) }
95
+ isMinimized () { return ipc.parent({ act: 'isMinimized', id: this.id }) }
96
+ isMaximized () { return ipc.parent({ act: 'isMaximized', id: this.id }) }
97
+ isFullscreen () { return ipc.parent({ act: 'isFullscreen', id: this.id }) }
98
+ isClosed () { return ipc.parent({ act: 'isClosed', id: this.id }) }
99
+ }
100
+
101
+ class App {
102
+ id = null
103
+ #untray = null
104
+
105
+ get parent () {
106
+ const parentId = ipc.getParentId()
107
+ Object.defineProperty(this, 'parent', { value: new Parent(parentId) })
108
+ return this.parent
109
+ }
110
+
111
+ constructor (id) {
112
+ this.id = id
113
+ this.tray.scaleFactor = state.tray?.scaleFactor
114
+ this.tray.darkMode = state.tray?.darkMode
115
+
116
+ ipc.systemTheme().on('data', ({ mode }) => {
117
+ this.tray.darkMode = mode === 'dark'
118
+ })
119
+ }
120
+
121
+ find = (options) => {
122
+ const found = new Found(this.id)
123
+ ipc.find({ id: this.id, options })
124
+ return found
125
+ }
126
+
127
+ badge = (count) => {
128
+ if (!Number.isInteger(+count)) throw new Error('argument must be an integer')
129
+ return ipc.badge({ id: this.id, count })
130
+ }
131
+
132
+ tray = async (opts = {}, listener) => {
133
+ opts = {
134
+ ...opts,
135
+ menu: opts.menu ?? {
136
+ show: `Show ${state.name}`,
137
+ quit: 'Quit'
138
+ }
139
+ }
140
+ listener = listener ?? ((key) => {
141
+ if (key === 'click' || key === 'show') {
142
+ this.show()
143
+ this.focus({ steal: true })
144
+ return
145
+ }
146
+ if (key === 'quit') {
147
+ this.quit()
148
+ }
149
+ })
150
+
151
+ const untray = async () => {
152
+ if (this.#untray) {
153
+ await this.#untray()
154
+ this.#untray = null
155
+ }
156
+ }
157
+
158
+ await untray()
159
+ this.#untray = ipc.tray(opts, listener)
160
+ return untray
161
+ }
162
+
163
+ focus = (options = null) => { return ipc.focus({ id: this.id, options }) }
164
+ blur = () => { return ipc.blur({ id: this.id }) }
165
+ show = () => { return ipc.show({ id: this.id }) }
166
+ hide = () => { return ipc.hide({ id: this.id }) }
167
+ minimize = () => { return ipc.minimize({ id: this.id }) }
168
+ maximize = () => { return ipc.maximize({ id: this.id }) }
169
+ fullscreen = () => { return ipc.fullscreen({ id: this.id }) }
170
+ restore = () => { return ipc.restore({ id: this.id }) }
171
+ close = () => { return ipc.close({ id: this.id }) }
172
+ quit = () => { return ipc.quit({ id: this.id }) }
173
+ dimensions (options = null) { return ipc.dimensions({ id: this.id, options }) }
174
+ isVisible = () => { return ipc.isVisible({ id: this.id }) }
175
+ isMinimized = () => { return ipc.isMinimized({ id: this.id }) }
176
+ isMaximized = () => { return ipc.isMaximized({ id: this.id }) }
177
+ isFullscreen = () => { return ipc.isFullscreen({ id: this.id }) }
178
+ report = (rpt) => { return ipc.report(rpt) }
179
+ }
180
+
181
+ class GuiCtrl extends EventEmitter {
182
+ #listener = null
183
+ #unlisten = null
184
+
185
+ static get parent () {
186
+ console.warn('Pear.Window.parent & Pear.View.parent are deprecated use ui.app.parent')
187
+ return Pear[Pear.constructor.UI].app.parent
188
+ }
189
+
190
+ static get self () {
191
+ console.warn('Pear.Window.self & Pear.View.self are deprecated use ui.app')
192
+ return Pear[Pear.constructor.UI].app
193
+ }
194
+
195
+ constructor (entry, at, options = at) {
196
+ super()
197
+ if (options === at) {
198
+ if (typeof at === 'string') options = { at }
199
+ }
200
+ if (!entry) throw new Error(`No path provided, cannot open ${this.constructor[kGuiCtrl]}`)
201
+ this.entry = entry
202
+ this.options = options
203
+ this.id = null
204
+ }
205
+
206
+ #rxtx () {
207
+ this.#listener = (e, ...args) => this.emit('message', ...args)
208
+ this.#unlisten = ipc.receiveFrom(this.#listener)
209
+ }
210
+
211
+ #unrxtx () {
212
+ if (this.#unlisten === null) return
213
+ this.#unlisten()
214
+ this.#unlisten = null
215
+ this.#listener = null
216
+ }
217
+
218
+ find = async (options) => {
219
+ const found = new Found(this.id)
220
+ await ipc.find({ id: this.id, options })
221
+ return found
222
+ }
223
+
224
+ send (...args) { return ipc.sendTo(this.id, ...args) }
225
+
226
+ async open (opts) {
227
+ if (this.id === null) {
228
+ await new Promise(setImmediate) // needed for windows/views opening on app load
229
+ this.#rxtx()
230
+ this.id = await ipc.ctrl({
231
+ parentId: Pear[Pear.constructor.UI].app.id,
232
+ type: this.constructor[kGuiCtrl],
233
+ entry: this.entry,
234
+ options: this.options,
235
+ state,
236
+ openOptions: opts
237
+ })
238
+ return true
239
+ }
240
+ return await ipc.open({ id: this.id })
241
+ }
242
+
243
+ async close () {
244
+ const result = await ipc.close({ id: this.id })
245
+ this.#unrxtx()
246
+ this.id = null
247
+ return result
248
+ }
249
+
250
+ show () { return ipc.show({ id: this.id }) }
251
+ hide () { return ipc.hide({ id: this.id }) }
252
+ focus (options = null) { return ipc.focus({ id: this.id, options }) }
253
+ blur () { return ipc.blur({ id: this.id }) }
254
+
255
+ dimensions (options = null) { return ipc.dimensions({ id: this.id, options }) }
256
+ minimize () {
257
+ if (this.constructor[kGuiCtrl] === 'view') throw new Error('A View cannot be minimized')
258
+ return ipc.minimize({ id: this.id })
259
+ }
260
+
261
+ maximize () {
262
+ if (this.constructor[kGuiCtrl] === 'view') throw new Error('A View cannot be maximized')
263
+ return ipc.maximize({ id: this.id })
264
+ }
265
+
266
+ fullscreen () {
267
+ if (this.constructor[kGuiCtrl] === 'view') throw new Error('A View cannot be fullscreened')
268
+ return ipc.fullscreen({ id: this.id })
269
+ }
270
+
271
+ restore () { return ipc.restore({ id: this.id }) }
272
+
273
+ isVisible () { return ipc.isVisible({ id: this.id }) }
274
+
275
+ isMinimized () {
276
+ if (this.constructor[kGuiCtrl] === 'view') throw new Error('A View cannot be minimized')
277
+ return ipc.isMinimized({ id: this.id })
278
+ }
279
+
280
+ isMaximized () {
281
+ if (this.constructor[kGuiCtrl] === 'view') throw new Error('A View cannot be maximized')
282
+ return ipc.isMaximized({ id: this.id })
283
+ }
284
+
285
+ isFullscreen () {
286
+ if (this.constructor[kGuiCtrl] === 'view') throw new Error('A View cannot be maximized')
287
+ return ipc.isFullscreen({ id: this.id })
288
+ }
289
+
290
+ isClosed () { return ipc.isClosed({ id: this.id }) }
291
+ }
292
+
293
+ class Window extends GuiCtrl {
294
+ static [kGuiCtrl] = 'window'
295
+ }
296
+
297
+ class View extends GuiCtrl { static [kGuiCtrl] = 'view' }
298
+
299
+ class PearElectron {
300
+ Window = Window
301
+ View = View
302
+ media = media
303
+ #app = null
304
+ get app () {
305
+ if (this.#app) return this.#app
306
+ this.#app = new App(ipc.getId())
307
+ return this.#app
308
+ }
309
+
310
+ warming () { return ipc.warming() }
311
+
312
+ async get (key) {
313
+ return Buffer.from(await ipc.get(key)).toString('utf-8')
314
+ }
315
+
316
+ constructor () {
317
+ if (state.isDecal) {
318
+ this.constructor.DECAL = {
319
+ ipc,
320
+ 'hypercore-id-encoding': require('hypercore-id-encoding'),
321
+ 'pear-api/constants': require('pear-api/constants')
322
+ }
323
+ }
324
+ }
325
+ }
326
+
327
+ this[this.constructor.UI] = new PearElectron()
328
+ }
329
+
330
+ get media () {
331
+ console.warn('Pear.media is deprecated use require(\'pear-electron\').media')
332
+ return this[this.constructor.UI].media
333
+ }
334
+
335
+ get Window () {
336
+ console.warn('Pear.Window is deprecated use require(\'pear-electron\').Window')
337
+ return this[this.constructor.UI].Window
338
+ }
339
+
340
+ get View () {
341
+ console.warn('Pear.View is deprecated use require(\'pear-electron\').View')
342
+ return this[this.constructor.UI].View
343
+ }
344
+
345
+ run (link, args = []) { return this.#ipc.run(link, args) }
346
+
347
+ get pipe () {
348
+ if (this.#pipe !== null) return this.#pipe
349
+ this.#pipe = this.#ipc.pipe()
350
+ return this.#pipe
351
+ }
352
+
353
+ exit = (code) => {
354
+ process.exitCode = code
355
+ this.ipc.processExit(code)
356
+ }
357
+ }
358
+ return API
359
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pear-electron",
3
- "version": "1.5.2",
3
+ "version": "1.7.0",
4
4
  "description": "Pear User-Interface Library for Electron",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -14,10 +14,21 @@
14
14
  "bootstrap.js",
15
15
  "index.js",
16
16
  "runtime.js",
17
- "pre.js"
17
+ "pre.js",
18
+ "api.js"
18
19
  ],
19
20
  "pear": {
20
- "bootstrap": "pear://0.2725.goowesg5dga9j1ryx47rsk9o4zms4541me4zerxsnbu8u99duh4o",
21
+ "assets": {
22
+ "ui": {
23
+ "link": "pear://0.2731.goowesg5dga9j1ryx47rsk9o4zms4541me4zerxsnbu8u99duh4o",
24
+ "name": "Pear Runtime",
25
+ "only": [
26
+ "/boot.bundle",
27
+ "/by-arch/%%HOST%%",
28
+ "/prebuilds/%%HOST%%"
29
+ ]
30
+ }
31
+ },
21
32
  "stage": {
22
33
  "skipWarmup": "true",
23
34
  "only": [
@@ -62,7 +73,7 @@
62
73
  "iambus": "^1.0.3",
63
74
  "localdrive": "^1.12.1",
64
75
  "paparam": "^1.6.1",
65
- "pear-api": "^1.17.6",
76
+ "pear-api": "^1.17.7",
66
77
  "pear-ipc": "^6.3.0",
67
78
  "script-linker": "^2.5.3",
68
79
  "streamx": "^2.20.2",
package/pre.js CHANGED
@@ -16,6 +16,9 @@ async function configure (options) {
16
16
  const entrypoints = srcs(html)
17
17
  stage.entrypoints = Array.isArray(stage.entrypoints) ? [...stage.entrypoints, ...entrypoints] : entrypoints
18
18
  options.stage = stage
19
+ const pkg = (options.assets?.ui && !options.assets.ui.only) ? null : JSON.parse(await drive.get('node_modules/pear-electron/package.json'))
20
+ options.assets = options.assets ?? pkg?.pear?.assets
21
+ options.assets.ui.only = options.assets?.ui?.only ?? pkg?.pear?.assets?.ui?.only
19
22
  return options
20
23
  }
21
24
 
package/runtime.js CHANGED
@@ -12,24 +12,19 @@ const constants = require('pear-api/constants')
12
12
  const plink = require('pear-api/link')
13
13
  const Logger = require('pear-api/logger')
14
14
  const { ERR_INVALID_APPLING, ERR_INVALID_PROJECT_DIR } = require('pear-api/errors')
15
- const { ansi, byteSize, indicator, outputter } = require('pear-api/terminal')
15
+
16
16
  const run = require('pear-api/cmd/run')
17
17
  const pear = require('pear-api/cmd')
18
18
  const pkg = require('./package.json')
19
19
 
20
- const bin = (name = Pear.config.name) => {
21
- const formatedName = name[0].toUpperCase() + name.slice(1)
22
- const app = isMac ? formatedName + ' Runtime.app' : name + '-runtime-app'
23
- const exe = isWindows ? formatedName + ' Runtime.exe' : (isMac ? 'Contents/MacOS/' + formatedName + ' Runtime' : name + '-runtime')
20
+ const bin = (name) => {
21
+ const kebab = name.toLowerCase().split(' ').join('-')
22
+ const cased = kebab.split('-').map((w) => w[0].toUpperCase() + w.slice(1)).join(' ')
23
+ const app = isMac ? cased + '.app' : kebab + '-app'
24
+ const exe = isWindows ? cased + '.exe' : (isMac ? 'Contents/MacOS/' + cased : kebab)
24
25
  return isWindows ? 'bin\\' + app + '\\' + exe : (isMac ? 'bin/' + app + '/' + exe : 'bin/' + app + '/' + exe)
25
26
  }
26
27
 
27
- const BIN = isWindows
28
- ? 'bin\\pear-runtime-app\\Pear Runtime.exe'
29
- : isMac
30
- ? 'bin/Pear Runtime.app/Contents/MacOS/Pear Runtime'
31
- : 'bin/pear-runtime-app/pear-runtime'
32
-
33
28
  class PearElectron {
34
29
  constructor () {
35
30
  this.ipc = Pear[Pear.constructor.IPC]
@@ -38,54 +33,9 @@ class PearElectron {
38
33
  Pear.teardown(() => this.ipc.close())
39
34
  }
40
35
 
41
- #outs () {
42
- if (this.LOG.INF === false) {
43
- return {
44
- stats ({ upload, download, peers }) {
45
- const dl = download.total + download.speed === 0 ? '' : `[${ansi.down} ${byteSize(download.total)} - ${byteSize(download.speed)}/s ] `
46
- const ul = upload.total + upload.speed === 0 ? '' : `[${ansi.up} ${byteSize(upload.total)} - ${byteSize(upload.speed)}/s ] `
47
- return {
48
- output: 'status',
49
- message: `Syncing Runtime [ Peers: ${peers} ] ${dl}${ul}`
50
- }
51
- },
52
- final (asset) {
53
- if (asset.forced === false && asset.inserted === false) return {}
54
- return 'Synced\x1b[K'
55
- }
56
- }
57
- }
58
- return {
59
- dumping: ({ link, dir }) => this.LOG.format('INF', 'Syncing runtime from peers\nfrom: ' + link + '\ninto: ' + dir + '\n'),
60
- byteDiff: ({ type, sizes, message }) => {
61
- sizes = sizes.map((size) => (size > 0 ? '+' : '') + byteSize(size)).join(', ')
62
- return this.LOG.format('INF', indicator(type, 'diff') + ' ' + message + ' (' + sizes + ')')
63
- },
64
- file: ({ key }) => this.LOG.format('INF', key),
65
- complete: () => this.LOG.format('INF', 'Asset fetch complete'),
66
- error: (err) => this.LOG.format('INF', `Asset fetch Failure (code: ${err.code || 'none'}) ${err.stack}`)
67
- }
68
- }
69
-
70
- async #asset (opts, force = false) {
71
- const output = outputter('asset', this.#outs())
72
- const json = false
73
- const bootstrap = opts.bootstrap?.link ?? opts.bootstrap ?? pkg.pear.bootstrap
74
- const executable = opts.bootstrap ? bin(opts.bootstrap.name) : BIN
75
- const stream = Pear.asset(bootstrap, {
76
- only: ['/boot.bundle', '/by-arch/' + require.addon.host, '/prebuilds/' + require.addon.host],
77
- force
78
- })
79
- const asset = await output(json, stream)
80
- if (asset === null || !asset?.path) throw new Error('Failed to determine runtime asset from sidecar')
81
- return path.join(asset.path, 'by-arch', require.addon.host, executable)
82
- }
83
-
84
36
  async start (opts = {}) {
85
37
  this.LOG.info('Fetching asset & determining bin path')
86
- this.bin = await this.#asset(opts)
87
- // if disk tampered then resync:
88
- if (fs.existsSync(this.bin) === false) this.bin = await this.#asset(opts, true)
38
+ this.bin = path.join(Pear.config.assets.ui.path, 'by-arch', require.addon.host, bin(Pear.config.assets.ui.name))
89
39
  const parsed = pear(Pear.argv.slice(1))
90
40
  const cmd = command('run', ...run)
91
41
  let argv = parsed.rest