@owear/cli 0.1.3 → 0.1.4
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/package.json +7 -7
- package/src/ow.js +83 -4
- package/template/README.md +34 -28
- package/template/app/main.ts +273 -4
- package/template/index.html +117 -28
- package/template/package.json +1 -1
- package/template/public/favicon.svg +4 -0
- package/template/src/assets/dancing-script.woff2 +0 -0
- package/template/src/assets/jetbrains-mono.woff2 +0 -0
- package/template/src/ow.d.ts +9 -1
- package/template/src/renderer.ts +489 -1
- package/template/src/style.css +280 -56
- package/template-installer/index.html +36 -26
- package/template-installer/src/assets/dancing-script.woff2 +0 -0
- package/template-installer/src/assets/jetbrains-mono.woff2 +0 -0
- package/template-installer/src/installer.ts +44 -25
- package/template-installer/src/style.css +429 -53
- package/template-installer/uninstaller/index.html +22 -16
- package/template-installer/uninstaller/src/uninstaller.ts +53 -28
- package/tools/owear-installer.mjs +5 -0
- package/tools/owear-sign.mjs +204 -0
- package/tools/owear-update.mjs +150 -0
- package/LICENSE +0 -201
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owear/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "CLI de Owear: create, dev, build",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"owear-build-native": "./src/owear-build-native.js"
|
|
9
9
|
},
|
|
10
10
|
"optionalDependencies": {
|
|
11
|
-
"@owear/linux-x64-gnu": "
|
|
12
|
-
"@owear/win32-x64": "
|
|
11
|
+
"@owear/linux-x64-gnu": "workspace:*",
|
|
12
|
+
"@owear/win32-x64": "workspace:*"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"src",
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"template",
|
|
18
18
|
"template-installer"
|
|
19
19
|
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"typecheck": "node --check src/ow.js && node --check src/owear-build-native.js && node --check tools/owear-pack.mjs && node --check tools/owear-installer.mjs && node --check tools/owear-linux.mjs && node --check tools/owear-msi.mjs && node --check tools/owear-update.mjs && node --check tools/owear-sign.mjs"
|
|
22
|
+
},
|
|
20
23
|
"license": "Apache-2.0",
|
|
21
24
|
"homepage": "https://owear.dev",
|
|
22
25
|
"repository": {
|
|
@@ -29,8 +32,5 @@
|
|
|
29
32
|
},
|
|
30
33
|
"engines": {
|
|
31
34
|
"node": ">=20"
|
|
32
|
-
},
|
|
33
|
-
"scripts": {
|
|
34
|
-
"typecheck": "node --check src/ow.js && node --check src/owear-build-native.js && node --check tools/owear-pack.mjs && node --check tools/owear-installer.mjs && node --check tools/owear-linux.mjs && node --check tools/owear-msi.mjs"
|
|
35
35
|
}
|
|
36
|
-
}
|
|
36
|
+
}
|
package/src/ow.js
CHANGED
|
@@ -148,6 +148,7 @@ async function main() {
|
|
|
148
148
|
case 'dev': return cmdDev(rest)
|
|
149
149
|
case 'build': return cmdBuild(rest)
|
|
150
150
|
case 'api': return cmdApi(rest)
|
|
151
|
+
case 'update': return cmdUpdate(rest)
|
|
151
152
|
default:
|
|
152
153
|
die(`comando desconocido: ${cmd} (usa --help)`)
|
|
153
154
|
}
|
|
@@ -167,9 +168,20 @@ ${C.cyan}owear${C.reset} — framework desktop nativo
|
|
|
167
168
|
${C.green}ow build uninstaller${C.reset} binario desinstalador
|
|
168
169
|
${C.green}ow api list${C.reset} lista las APIs del repo y sus manifiestos
|
|
169
170
|
${C.green}ow api new <nombre>${C.reset} scaffoldea una API (api/<nombre>/ + manifiesto)
|
|
171
|
+
${C.green}ow update --file <bin> --version <v>${C.reset} publica un update (blockmap + manifiesto YAML firmado)
|
|
172
|
+
${C.green}ow update --gen-key <ruta>${C.reset} genera un par de claves Ed25519 para firmar
|
|
170
173
|
|
|
171
174
|
Variables útiles:
|
|
172
175
|
OW_KERNEL_BIN ruta al binario owear
|
|
176
|
+
OW_SIGN_KEY clave privada Ed25519 para firmar artefactos (→ <file>.sig)
|
|
177
|
+
OW_SIGN_PFX certificado PKCS#12 para Authenticode (Windows PE/MSI)
|
|
178
|
+
OW_SIGN_PFX_PASSWORD contraseña del PFX (o --pfx-password-env)
|
|
179
|
+
|
|
180
|
+
Firma (ow build app|installer|uninstaller):
|
|
181
|
+
--sign-key <pem> firma Ed25519 desprendida del artefacto
|
|
182
|
+
--pfx <p12> Authenticode (requiere osslsigncode/signtool)
|
|
183
|
+
--timestamp <url> sellado de tiempo Authenticode
|
|
184
|
+
--require-sign falla el build si no se firma
|
|
173
185
|
`)
|
|
174
186
|
}
|
|
175
187
|
|
|
@@ -380,7 +392,7 @@ function findMainEntry(cwd) {
|
|
|
380
392
|
* así que compilamos con esbuild (viene con vite). De este modo el runtime del
|
|
381
393
|
* sistema sirve y el arranque no depende de la versión que tenga el usuario.
|
|
382
394
|
*/
|
|
383
|
-
function prepareMain(cwd, outDir) {
|
|
395
|
+
function prepareMain(cwd, outDir, bundle = false) {
|
|
384
396
|
const entry = findMainEntry(cwd)
|
|
385
397
|
if (!entry) return null
|
|
386
398
|
if (entry.endsWith('.js') || entry.endsWith('.mjs')) return entry
|
|
@@ -393,7 +405,11 @@ function prepareMain(cwd, outDir) {
|
|
|
393
405
|
[
|
|
394
406
|
'esbuild', entry,
|
|
395
407
|
'--bundle', '--platform=node', '--format=esm',
|
|
396
|
-
|
|
408
|
+
// En producción se EMBEBE @owear/core (y deps) en main.js: el payload de
|
|
409
|
+
// un single-binary/instalador no lleva node_modules. En dev se dejan
|
|
410
|
+
// externas (node_modules está presente).
|
|
411
|
+
...(bundle ? [] : ['--packages=external']),
|
|
412
|
+
`--outfile=${out}`, '--log-level=warning',
|
|
397
413
|
],
|
|
398
414
|
{ cwd, stdio: 'inherit', shell: process.platform === 'win32' }
|
|
399
415
|
)
|
|
@@ -538,7 +554,7 @@ async function cmdBuildBundle(cwd) {
|
|
|
538
554
|
const code = await runProc('npx', ['vite', 'build'], { cwd, shell: process.platform === 'win32' })
|
|
539
555
|
if (code !== 0) die('vite build falló')
|
|
540
556
|
|
|
541
|
-
const mainJs = prepareMain(cwd, path.join(cwd, 'dist'))
|
|
557
|
+
const mainJs = prepareMain(cwd, path.join(cwd, 'dist'), true)
|
|
542
558
|
|
|
543
559
|
const workersDir = prepareWorkers(cwd, path.join(cwd, 'dist'))
|
|
544
560
|
if (workersDir) log(`workers: ${path.relative(cwd, workersDir)}`)
|
|
@@ -695,6 +711,52 @@ function runTool(name, args) {
|
|
|
695
711
|
if (r.status !== 0) die(`falló ${name}`)
|
|
696
712
|
}
|
|
697
713
|
|
|
714
|
+
/**
|
|
715
|
+
* Firma un artefacto recién construido. Authenticode si hay `--pfx`
|
|
716
|
+
* (`OW_SIGN_PFX`), Ed25519 desprendida (`<file>.sig`) si hay `--sign-key`
|
|
717
|
+
* (`OW_SIGN_KEY`). Nunca rompe el build: sin material de firma no hace nada;
|
|
718
|
+
* con `--require-sign` la firma pasa a ser obligatoria.
|
|
719
|
+
*/
|
|
720
|
+
function signArtifact(args, file) {
|
|
721
|
+
const key = flag(args, '--sign-key', process.env.OW_SIGN_KEY ?? '')
|
|
722
|
+
const pfx = flag(args, '--pfx', process.env.OW_SIGN_PFX ?? '')
|
|
723
|
+
if (!key && !pfx) return
|
|
724
|
+
const extra = []
|
|
725
|
+
if (key) extra.push('--ed25519-key', key)
|
|
726
|
+
if (pfx) {
|
|
727
|
+
extra.push('--pfx', pfx, '--pfx-password-env', flag(args, '--pfx-password-env', 'OW_SIGN_PFX_PASSWORD'))
|
|
728
|
+
}
|
|
729
|
+
const ts = flag(args, '--timestamp', process.env.OW_SIGN_TIMESTAMP ?? '')
|
|
730
|
+
if (ts) extra.push('--timestamp', ts)
|
|
731
|
+
if (args.includes('--require-sign')) extra.push('--require')
|
|
732
|
+
runTool('owear-sign.mjs', ['--file', file, ...extra])
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* ow update — publica un update del auto-updater: calcula el blockmap (delta
|
|
737
|
+
* por bloques), genera el manifiesto YAML del canal y lo firma (Ed25519).
|
|
738
|
+
* Delega en tools/owear-update.mjs (mismos flags).
|
|
739
|
+
*/
|
|
740
|
+
function cmdUpdate(args) {
|
|
741
|
+
if (!args.length) {
|
|
742
|
+
console.log(`
|
|
743
|
+
${C.cyan}ow update${C.reset} — publica un update (delta + manifiesto YAML firmado)
|
|
744
|
+
|
|
745
|
+
${C.green}ow update --gen-key owear-signing${C.reset}
|
|
746
|
+
genera el par de claves Ed25519 (privada <ruta>.pem; pública <ruta>.pub.b64,
|
|
747
|
+
que va en el bridge como ${C.dim}updater.publicKey${C.reset})
|
|
748
|
+
|
|
749
|
+
${C.green}ow update --file release/MiApp-1.4.0 --version 1.4.0 --key owear-signing.pem${C.reset} \\
|
|
750
|
+
[--channel latest] [--url https://up.miapp.dev] [--notes "…"|--notes @notas.md] \\
|
|
751
|
+
[--block-size 262144] [--mandatory] [--out-dir release]
|
|
752
|
+
|
|
753
|
+
Salidas: ${C.dim}<file>.blockmap${C.reset} (bloques sha256) y ${C.dim}<channel>.yml${C.reset} (manifiesto YAML).
|
|
754
|
+
`)
|
|
755
|
+
return
|
|
756
|
+
}
|
|
757
|
+
runTool('owear-update.mjs', args)
|
|
758
|
+
}
|
|
759
|
+
|
|
698
760
|
function readPkg(cwd) {
|
|
699
761
|
try {
|
|
700
762
|
return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'))
|
|
@@ -771,6 +833,7 @@ async function cmdBuildApp(cwd, args) {
|
|
|
771
833
|
const out = path.join(outDir, exe)
|
|
772
834
|
packDir(kernel, bundle, out)
|
|
773
835
|
log(`binario único: ${out}`)
|
|
836
|
+
signArtifact(args, out)
|
|
774
837
|
return
|
|
775
838
|
}
|
|
776
839
|
|
|
@@ -799,6 +862,7 @@ async function cmdBuildApp(cwd, args) {
|
|
|
799
862
|
runTool('owear-linux.mjs', ['deb', '--stage', stage, '--out', out, '--name', slug,
|
|
800
863
|
'--version', meta.version, '--maintainer', meta.publisher, '--description', meta.appName])
|
|
801
864
|
log(`.deb: ${out}`)
|
|
865
|
+
signArtifact(args, out)
|
|
802
866
|
return
|
|
803
867
|
}
|
|
804
868
|
|
|
@@ -823,6 +887,7 @@ async function cmdBuildApp(cwd, args) {
|
|
|
823
887
|
const out = path.join(outDir, `${slug}-${meta.version}-x86_64.AppImage`)
|
|
824
888
|
runTool('owear-linux.mjs', ['appimage', '--appdir', appDir, '--out', out, '--arch', 'x86_64'])
|
|
825
889
|
log(`AppImage: ${out}`)
|
|
890
|
+
signArtifact(args, out)
|
|
826
891
|
return
|
|
827
892
|
}
|
|
828
893
|
|
|
@@ -833,6 +898,7 @@ async function cmdBuildApp(cwd, args) {
|
|
|
833
898
|
runTool('owear-msi.mjs', ['--stage', stage, '--out', out, '--name', slug,
|
|
834
899
|
'--version', meta.version, '--publisher', meta.publisher, '--appId', meta.appId])
|
|
835
900
|
log(`.MSI: ${out}`)
|
|
901
|
+
signArtifact(args, out)
|
|
836
902
|
return
|
|
837
903
|
}
|
|
838
904
|
|
|
@@ -869,7 +935,14 @@ async function cmdBuildInstaller(cwd, args, isUninstaller) {
|
|
|
869
935
|
// 1b) desinstalador (si existe installer/uninstaller) → se instala junto a la app
|
|
870
936
|
if (fs.existsSync(path.join(cwd, 'installer', 'uninstaller'))) {
|
|
871
937
|
log('build del desinstalador…')
|
|
872
|
-
|
|
938
|
+
// propaga solo los flags de firma (no --mode/--format, que no aplican)
|
|
939
|
+
const signArgs = []
|
|
940
|
+
for (const f of ['--sign-key', '--pfx', '--pfx-password-env', '--timestamp']) {
|
|
941
|
+
const v = flag(args, f, null)
|
|
942
|
+
if (v) signArgs.push(f, v)
|
|
943
|
+
}
|
|
944
|
+
if (args.includes('--require-sign')) signArgs.push('--require-sign')
|
|
945
|
+
await cmdBuildInstaller(cwd, signArgs, true)
|
|
873
946
|
const un = path.join(outDir, `${slug}-uninstaller${exe}`)
|
|
874
947
|
if (fs.existsSync(un)) fs.copyFileSync(un, path.join(payloadDir, path.basename(un)))
|
|
875
948
|
}
|
|
@@ -879,11 +952,15 @@ async function cmdBuildInstaller(cwd, args, isUninstaller) {
|
|
|
879
952
|
const ui = await buildInstallerUI(cwd, isUninstaller ? 'uninstaller' : 'installer')
|
|
880
953
|
|
|
881
954
|
// 3) metadatos (installer.json / uninstaller.json) — el kernel los detecta
|
|
955
|
+
const iconRel = bridge.obj?.app?.icon
|
|
956
|
+
const iconAbs = iconRel ? path.resolve(cwd, iconRel) : ''
|
|
957
|
+
const iconOk = Boolean(iconAbs) && fs.existsSync(iconAbs)
|
|
882
958
|
const meta = {
|
|
883
959
|
appId: bridge.obj?.app?.id ?? slug,
|
|
884
960
|
appName: bridge.obj?.app?.name ?? slug,
|
|
885
961
|
version: bridge.obj?.app?.version ?? '0.0.0',
|
|
886
962
|
publisher: bridge.obj?.app?.publisher ?? null,
|
|
963
|
+
icon: iconOk ? 'icon' + path.extname(iconAbs) : null,
|
|
887
964
|
mode: isUninstaller ? 'uninstaller' : mode,
|
|
888
965
|
layout: target.layout ?? mode,
|
|
889
966
|
order: bridge.obj?.order ?? null,
|
|
@@ -909,9 +986,11 @@ async function cmdBuildInstaller(cwd, args, isUninstaller) {
|
|
|
909
986
|
'--name', slug,
|
|
910
987
|
]
|
|
911
988
|
if (payloadDir) toolArgs.push('--payload', payloadDir)
|
|
989
|
+
if (iconOk) toolArgs.push('--icon', iconAbs)
|
|
912
990
|
const r = spawnSync(process.execPath, toolArgs, { stdio: 'inherit' })
|
|
913
991
|
if (r.status !== 0) die(`falló owear-installer (${kind})`)
|
|
914
992
|
log(`${kind}: ${out}`)
|
|
993
|
+
signArtifact(args, out)
|
|
915
994
|
}
|
|
916
995
|
|
|
917
996
|
main().catch((e) => die(e?.stack ?? String(e)))
|
package/template/README.md
CHANGED
|
@@ -3,54 +3,54 @@
|
|
|
3
3
|
|
|
4
4
|
# __APP_NAME__
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
Desktop app built with **[Owear](https://owear.dev)**: the system WebView renders
|
|
7
|
+
your web frontend and the native kernel gives you the operating system.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Get started
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npm install
|
|
13
13
|
npm run dev # ≡ ow dev
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
hot-reload
|
|
16
|
+
A window with the system WebView opens. Edit `src/renderer.ts` and Vite's
|
|
17
|
+
hot-reload reflects it instantly.
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
npm run build # ≡ ow build → dist/
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
##
|
|
23
|
+
## Structure
|
|
24
24
|
|
|
25
25
|
```
|
|
26
|
-
├── index.html UI (
|
|
26
|
+
├── index.html UI (custom title bar, hero, cards)
|
|
27
27
|
├── src/
|
|
28
|
-
│ ├── renderer.ts
|
|
29
|
-
│ ├── style.css
|
|
30
|
-
│ └── ow.d.ts
|
|
28
|
+
│ ├── renderer.ts renderer logic + calls to native modules
|
|
29
|
+
│ ├── style.css styles
|
|
30
|
+
│ └── ow.d.ts types for window.ow
|
|
31
31
|
└── app/
|
|
32
|
-
└── main.ts
|
|
32
|
+
└── main.ts main process (Node sidecar): just the window
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
##
|
|
35
|
+
## How it talks to the system
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
No `ipcRenderer` and no `ipcMain`: the renderer calls the kernel's native modules
|
|
38
|
+
directly.
|
|
39
39
|
|
|
40
40
|
```ts
|
|
41
|
-
const path = await ow.invoke('dialog', 'open', 'open', '
|
|
41
|
+
const path = await ow.invoke('dialog', 'open', 'open', 'Open a file')
|
|
42
42
|
const text = await ow.invoke('fs', 'readText', path)
|
|
43
|
-
await ow.invoke('notification', 'show', '
|
|
43
|
+
await ow.invoke('notification', 'show', 'Done', `Read ${text.length} bytes`)
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
`
|
|
48
|
-
`globalshortcut` · `updater` · `capturer`,
|
|
49
|
-
`window` (extras
|
|
46
|
+
Stock modules: `fs` · `path` · `process` (PTY) · `dialog` · `clipboard` ·
|
|
47
|
+
`shell` · `screen` · `net` · `notification` · `power` · `menu` ·
|
|
48
|
+
`globalshortcut` · `updater` · `capturer`, plus the builtins `ow-window`
|
|
49
|
+
(window), `window` (WebView extras), `session`, `app`, and `crashreporter`.
|
|
50
50
|
|
|
51
|
-
##
|
|
51
|
+
## Adding your own native module (C++)
|
|
52
52
|
|
|
53
|
-
1.
|
|
53
|
+
1. Create `native/my_module.cpp`:
|
|
54
54
|
|
|
55
55
|
```cpp
|
|
56
56
|
#include <ow/Json.h>
|
|
@@ -61,17 +61,23 @@ Módulos incluidos (stock): `fs` · `path` · `process` (PTY) · `dialog` ·
|
|
|
61
61
|
ow::Module::RespondOk(res, "\"pong\"");
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
OW_MODULE_BEGIN(
|
|
64
|
+
OW_MODULE_BEGIN(my_module, "1.0.0")
|
|
65
65
|
OW_FN(ping)
|
|
66
66
|
OW_MODULE_END()
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
2. `ow dev`
|
|
69
|
+
2. `ow dev` compiles it to `.owm` and generates the binding. Use it in the
|
|
70
|
+
renderer:
|
|
70
71
|
|
|
71
72
|
```ts
|
|
72
|
-
import {
|
|
73
|
-
await
|
|
73
|
+
import { my_module } from '@owear/native'
|
|
74
|
+
await my_module.ping()
|
|
74
75
|
```
|
|
75
76
|
|
|
76
|
-
>
|
|
77
|
-
> `native/`
|
|
77
|
+
> Requires a C++ compiler on the system. If you do not have one, do not add
|
|
78
|
+
> `native/` and the app keeps working with the stock modules.
|
|
79
|
+
|
|
80
|
+
## Learn more
|
|
81
|
+
|
|
82
|
+
The full documentation lives in the Owear repository under
|
|
83
|
+
[`docs/`](https://github.com/owear/owear/tree/main/docs).
|
package/template/app/main.ts
CHANGED
|
@@ -3,21 +3,290 @@
|
|
|
3
3
|
//
|
|
4
4
|
// app/main.ts — proceso principal (sidecar Node), estilo Electron.
|
|
5
5
|
//
|
|
6
|
-
// Sólo gestiona el ciclo de vida de la ventana. La app
|
|
7
|
-
//
|
|
8
|
-
// lógica privilegiada (por ejemplo, exponerla vía un módulo nativo propio).
|
|
6
|
+
// Sólo gestiona el ciclo de vida de la ventana. La app vive en el renderer,
|
|
7
|
+
// que llama a los módulos nativos directo.
|
|
9
8
|
|
|
10
|
-
import { app, BrowserWindow } from '@owear/core'
|
|
9
|
+
import { app, BrowserWindow, Menu, Tray, invokeNative, nativeImage, theme, screen, powerMonitor, powerSaveBlocker } from '@owear/core'
|
|
10
|
+
import * as fs from 'node:fs'
|
|
11
|
+
import * as os from 'node:os'
|
|
12
|
+
import * as path from 'node:path'
|
|
11
13
|
|
|
12
14
|
app.whenReady().then(() => {
|
|
15
|
+
// Icono de la app: `favicon.svg` del proyecto (Vite lo copia a dist/ en build).
|
|
16
|
+
// `app.setIcon` fija el icono por defecto de las ventanas creadas después.
|
|
17
|
+
// (API de icono del kernel: `window.setIcon`.)
|
|
18
|
+
const iconPath = [
|
|
19
|
+
path.join(app.getAppPath(), 'favicon.svg'), // empaquetado: dist/ → app/
|
|
20
|
+
path.join(process.cwd(), 'public', 'favicon.svg'), // dev: public/ del proyecto
|
|
21
|
+
].find((p) => fs.existsSync(p))
|
|
22
|
+
if (iconPath) app.setIcon(iconPath)
|
|
23
|
+
|
|
13
24
|
const win = new BrowserWindow({
|
|
14
25
|
title: '__APP_NAME__',
|
|
15
26
|
width: 1080,
|
|
16
27
|
height: 720,
|
|
17
28
|
titleBarStyle: 'custom', // titlebar propia + regiones [data-ow-drag]
|
|
29
|
+
// Botones de ventana nativos (min/max/close) dentro de la titlebar custom.
|
|
30
|
+
// En Linux son los del TEMA (la distro define forma/tamaño/hover).
|
|
31
|
+
// Opcional: color = fondo de la banda · symbolColor = glifo ·
|
|
32
|
+
// buttonColor = fondo interno del círculo (si no, los del tema).
|
|
33
|
+
titleBarOverlay: { height: 40, color: '#21252b' },
|
|
18
34
|
url: process.env.OW_DEV_SERVER_URL ?? 'app://index.html',
|
|
19
35
|
})
|
|
20
36
|
|
|
21
37
|
win.on('ready-to-show', () => console.log('[main] ventana lista, id =', win.id))
|
|
22
38
|
win.on('closed', () => app.quit())
|
|
39
|
+
|
|
40
|
+
// ── C3: webContents (capturePage, eventos, setWindowOpenHandler) ─────────
|
|
41
|
+
app.handle('starter.capture', async () => {
|
|
42
|
+
const img = await win.webContents.capturePage()
|
|
43
|
+
return { data: img.toPNG().toString('base64') }
|
|
44
|
+
})
|
|
45
|
+
win.webContents.on('did-finish-load', () =>
|
|
46
|
+
void app.send('webContents.event', 'did-finish-load')
|
|
47
|
+
)
|
|
48
|
+
win.webContents.on('before-input-event', (e: { key?: string }) =>
|
|
49
|
+
void app.send('webContents.event', `before-input-event ${e?.key ?? ''}`)
|
|
50
|
+
)
|
|
51
|
+
win.webContents.setWindowOpenHandler(({ url }) => {
|
|
52
|
+
void app.send('webContents.event', `window-open ${url}`)
|
|
53
|
+
return { action: 'deny' }
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// ── C4: nativeImage (captura → resize/crop/toPNG/dataURL) ────────────────
|
|
57
|
+
app.handle('starter.nativeImage', async () => {
|
|
58
|
+
const shot = await win.webContents.capturePage() // NativeImage
|
|
59
|
+
const small = shot.resize({ width: 64 })
|
|
60
|
+
const fromBuffer = nativeImage.createFromBuffer(shot.toPNG())
|
|
61
|
+
const fromUrl = nativeImage.createFromDataURL(small.toDataURL())
|
|
62
|
+
const cropped = shot.crop({ x: 0, y: 0, width: 16, height: 16 })
|
|
63
|
+
return {
|
|
64
|
+
original: shot.getSize(),
|
|
65
|
+
resized: small.getSize(),
|
|
66
|
+
cropped: cropped.getSize(),
|
|
67
|
+
pngBytes: small.toPNG().length,
|
|
68
|
+
reDecoded: fromBuffer.getSize(),
|
|
69
|
+
fromDataURL: fromUrl.getSize(),
|
|
70
|
+
empty: small.isEmpty(),
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// ── C5: menu (contextual + menubar) ──────────────────────────────────────
|
|
75
|
+
// Menubar (Windows nativo; noop en Linux/GNOME por diseño).
|
|
76
|
+
Menu.setApplicationMenu(
|
|
77
|
+
Menu.buildFromTemplate([
|
|
78
|
+
{
|
|
79
|
+
label: 'Archivo',
|
|
80
|
+
submenu: [
|
|
81
|
+
{ label: 'Nuevo', accelerator: 'CmdOrCtrl+N', click: () => void app.send('menu.event', 'Nuevo') },
|
|
82
|
+
{ type: 'separator' },
|
|
83
|
+
{ role: 'quit' },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
label: 'Ver',
|
|
88
|
+
submenu: [
|
|
89
|
+
{
|
|
90
|
+
label: 'Barra lateral',
|
|
91
|
+
type: 'checkbox',
|
|
92
|
+
checked: true,
|
|
93
|
+
click: (mi) => void app.send('menu.event', `sidebar=${mi.checked}`),
|
|
94
|
+
},
|
|
95
|
+
{ type: 'separator' },
|
|
96
|
+
{ role: 'toggleDevTools' },
|
|
97
|
+
{ role: 'reload' },
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
])
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// Menú contextual (popup) disparado por un botón del starter.
|
|
104
|
+
app.handle('starter.menu', () => {
|
|
105
|
+
Menu.buildFromTemplate([
|
|
106
|
+
{ label: 'Nuevo', click: () => void app.send('menu.event', 'Nuevo') },
|
|
107
|
+
{ label: 'Abrir', accelerator: 'CmdOrCtrl+O', click: () => void app.send('menu.event', 'Abrir') },
|
|
108
|
+
{ type: 'separator' },
|
|
109
|
+
{
|
|
110
|
+
label: 'Barra lateral',
|
|
111
|
+
type: 'checkbox',
|
|
112
|
+
checked: true,
|
|
113
|
+
click: (mi) => void app.send('menu.event', `checkbox=${mi.checked}`),
|
|
114
|
+
},
|
|
115
|
+
{ label: 'Zona A', type: 'radio', checked: true, click: () => void app.send('menu.event', 'radio A') },
|
|
116
|
+
{ label: 'Zona B', type: 'radio', click: () => void app.send('menu.event', 'radio B') },
|
|
117
|
+
{ type: 'separator' },
|
|
118
|
+
{ role: 'copy' },
|
|
119
|
+
{ role: 'toggleDevTools' },
|
|
120
|
+
{ role: 'quit' },
|
|
121
|
+
]).popup({ window: win })
|
|
122
|
+
return null
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// ── C6: tray (icono de bandeja) ──────────────────────────────────────────
|
|
126
|
+
let tray: Tray | null = null
|
|
127
|
+
app.handle('starter.tray', async () => {
|
|
128
|
+
if (tray) return { created: false, id: tray.id }
|
|
129
|
+
const icon = (await win.webContents.capturePage()).resize({ width: 16, height: 16 })
|
|
130
|
+
tray = new Tray(icon)
|
|
131
|
+
tray.setToolTip('__APP_NAME__')
|
|
132
|
+
tray.setTitle('__APP_NAME__')
|
|
133
|
+
tray.setContextMenu(
|
|
134
|
+
Menu.buildFromTemplate([
|
|
135
|
+
{ label: 'Mostrar ventana', click: () => void win.show() },
|
|
136
|
+
{ label: 'Capturar página', click: () => void app.send('tray.event', 'capturar') },
|
|
137
|
+
{ type: 'separator' },
|
|
138
|
+
{ role: 'quit' },
|
|
139
|
+
])
|
|
140
|
+
)
|
|
141
|
+
tray.on('click', () => {
|
|
142
|
+
void win.show()
|
|
143
|
+
void app.send('tray.event', 'click')
|
|
144
|
+
})
|
|
145
|
+
tray.on('right-click', () => void app.send('tray.event', 'right-click'))
|
|
146
|
+
tray.on('double-click', () => void app.send('tray.event', 'double-click'))
|
|
147
|
+
return { created: true, id: tray.id }
|
|
148
|
+
})
|
|
149
|
+
app.handle('starter.trayDestroy', () => {
|
|
150
|
+
tray?.destroy()
|
|
151
|
+
tray = null
|
|
152
|
+
return null
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// ── C7: nativeTheme (leer/forzar/escuchar) ───────────────────────────────
|
|
156
|
+
app.handle('starter.themeGet', () => theme.get())
|
|
157
|
+
app.handle('starter.themeSet', async (src: string) => {
|
|
158
|
+
await theme.setSource((src as 'system' | 'light' | 'dark') ?? 'system')
|
|
159
|
+
return theme.get()
|
|
160
|
+
})
|
|
161
|
+
void theme.watch() // cambios del sistema → ow.on('theme.changed')
|
|
162
|
+
|
|
163
|
+
// ── C8: print / printToPDF ───────────────────────────────────────────────
|
|
164
|
+
app.handle('starter.printPdf', async () => {
|
|
165
|
+
try {
|
|
166
|
+
const pdf = await win.webContents.printToPDF()
|
|
167
|
+
const out = path.join(os.tmpdir(), 'owear-starter.pdf')
|
|
168
|
+
fs.writeFileSync(out, pdf)
|
|
169
|
+
void invokeNative('shell', 'showItemInFolder', out)
|
|
170
|
+
return { ok: true, bytes: pdf.length, path: out }
|
|
171
|
+
} catch (e) {
|
|
172
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) }
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
app.handle('starter.print', () => {
|
|
176
|
+
win.webContents.print()
|
|
177
|
+
return null
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// ── C9: BrowserWindow completo ───────────────────────────────────────────
|
|
181
|
+
let child: BrowserWindow | null = null
|
|
182
|
+
app.handle('starter.childWindow', () => {
|
|
183
|
+
if (child) {
|
|
184
|
+
void child.focus()
|
|
185
|
+
return { id: child.id }
|
|
186
|
+
}
|
|
187
|
+
child = new BrowserWindow({
|
|
188
|
+
title: 'Ventana secundaria',
|
|
189
|
+
width: 520,
|
|
190
|
+
height: 360,
|
|
191
|
+
parent: win.id ?? undefined,
|
|
192
|
+
backgroundColor: '#12161c',
|
|
193
|
+
url: process.env.OW_DEV_SERVER_URL ?? 'app://index.html',
|
|
194
|
+
})
|
|
195
|
+
child.on('closed', () => {
|
|
196
|
+
child = null
|
|
197
|
+
})
|
|
198
|
+
return { id: child.id }
|
|
199
|
+
})
|
|
200
|
+
app.handle('starter.toggleOnTop', async () => {
|
|
201
|
+
const on = await win.isAlwaysOnTop()
|
|
202
|
+
await win.setAlwaysOnTop(!on)
|
|
203
|
+
return { on: !on }
|
|
204
|
+
})
|
|
205
|
+
app.handle('starter.progress', async () => {
|
|
206
|
+
await win.setProgressBar(0.5)
|
|
207
|
+
setTimeout(() => void win.setProgressBar(-1, { mode: 'none' }), 4000)
|
|
208
|
+
return null
|
|
209
|
+
})
|
|
210
|
+
app.handle('starter.ignoreMouse', () => {
|
|
211
|
+
void win.setIgnoreMouseEvents(true, { forward: true })
|
|
212
|
+
setTimeout(() => void win.setIgnoreMouseEvents(false), 3000)
|
|
213
|
+
return null
|
|
214
|
+
})
|
|
215
|
+
app.handle('starter.windows', async () => {
|
|
216
|
+
const focus = await BrowserWindow.getFocusedWindow()
|
|
217
|
+
const all = await BrowserWindow.getAllWindows()
|
|
218
|
+
return { focused: focus?.id ?? null, count: all.length, ids: all.map((w) => w.id) }
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// ── C10: pantallas (multi-monitor) y energía ─────────────────────────────
|
|
222
|
+
app.handle('starter.screens', async () => {
|
|
223
|
+
await screen.watch()
|
|
224
|
+
const displays = await screen.getAllDisplays()
|
|
225
|
+
const primary = displays.find((d) => d.primary) ?? displays[0] ?? null
|
|
226
|
+
const cursor = await screen.getCursorScreenPoint()
|
|
227
|
+
return { displays, primaryId: primary?.id ?? null, cursor }
|
|
228
|
+
})
|
|
229
|
+
app.handle('starter.nearest', async () => {
|
|
230
|
+
const cursor = await screen.getCursorScreenPoint()
|
|
231
|
+
const display = await screen.getDisplayNearestPoint(cursor)
|
|
232
|
+
return { cursor, display }
|
|
233
|
+
})
|
|
234
|
+
app.handle('starter.powerOn', async () => {
|
|
235
|
+
await powerMonitor.watch()
|
|
236
|
+
const onBattery = await powerMonitor.isOnBatteryPower()
|
|
237
|
+
const idle = await powerMonitor.getIdleTime()
|
|
238
|
+
return { onBattery, idle }
|
|
239
|
+
})
|
|
240
|
+
app.handle('starter.idle', async () => {
|
|
241
|
+
const idleTime = await powerMonitor.getIdleTime()
|
|
242
|
+
const state = await powerMonitor.getIdleState(30)
|
|
243
|
+
return { idleTime, state }
|
|
244
|
+
})
|
|
245
|
+
let blockerId: number | null = null
|
|
246
|
+
app.handle('starter.blocker', () => {
|
|
247
|
+
if (blockerId != null) {
|
|
248
|
+
const ok = powerSaveBlocker.stop(blockerId)
|
|
249
|
+
blockerId = null
|
|
250
|
+
return { on: false, ok }
|
|
251
|
+
}
|
|
252
|
+
blockerId = powerSaveBlocker.start('prevent-display-sleep')
|
|
253
|
+
return { on: true, id: blockerId }
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// ── C1: exponer `app` al renderer para probarlo en el starter ──────────────
|
|
258
|
+
// El renderer llama `ow.invoke('node','call',{fn:'starter.*'})`.
|
|
259
|
+
app.handle('starter.appInfo', () => ({
|
|
260
|
+
name: app.getName(),
|
|
261
|
+
version: app.getVersion(),
|
|
262
|
+
packaged: app.isPackaged(),
|
|
263
|
+
appPath: app.getAppPath(),
|
|
264
|
+
}))
|
|
265
|
+
|
|
266
|
+
const APP_PATH_NAMES = [
|
|
267
|
+
'home',
|
|
268
|
+
'userData',
|
|
269
|
+
'temp',
|
|
270
|
+
'logs',
|
|
271
|
+
'downloads',
|
|
272
|
+
'documents',
|
|
273
|
+
'exe',
|
|
274
|
+
'appPath',
|
|
275
|
+
] as const
|
|
276
|
+
|
|
277
|
+
app.handle('starter.appPaths', () => {
|
|
278
|
+
const out: Record<string, string> = {}
|
|
279
|
+
for (const name of APP_PATH_NAMES) out[name] = app.getPath(name)
|
|
280
|
+
return out
|
|
23
281
|
})
|
|
282
|
+
|
|
283
|
+
// Ciclo de vida de la app → renderer (app.send → ow.on('app.event')).
|
|
284
|
+
for (const ev of [
|
|
285
|
+
'window-all-closed',
|
|
286
|
+
'before-quit',
|
|
287
|
+
'will-quit',
|
|
288
|
+
'second-instance',
|
|
289
|
+
'child-process-gone',
|
|
290
|
+
] as const) {
|
|
291
|
+
app.on(ev, () => app.send('app.event', ev))
|
|
292
|
+
}
|