@owear/cli 0.1.0 → 0.1.2
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 +4 -3
- package/src/ow.js +34 -4
- package/template/README.md +77 -0
- package/template/app/main.ts +10 -16
- package/template/index.html +82 -21
- package/template/package.json +5 -2
- package/template/src/ow.d.ts +25 -0
- package/template/src/renderer.ts +132 -18
- package/template/src/style.css +342 -0
- package/template/tsconfig.json +3 -2
- package/template/vite.config.ts +3 -0
- package/template/native/files.cpp +0 -42
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owear/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "CLI de Owear: create, dev, build",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"owear-build-native": "./src/owear-build-native.js"
|
|
9
9
|
},
|
|
10
10
|
"optionalDependencies": {
|
|
11
|
-
"@owear/
|
|
11
|
+
"@owear/win32-x64": "0.1.2",
|
|
12
|
+
"@owear/linux-x64-gnu": "0.1.2"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"src",
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
"homepage": "https://owear.dev",
|
|
19
20
|
"repository": {
|
|
20
21
|
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/
|
|
22
|
+
"url": "git+https://github.com/Scrakk-Team/Owear-Framework.git",
|
|
22
23
|
"directory": "packages/cli"
|
|
23
24
|
},
|
|
24
25
|
"publishConfig": {
|
package/src/ow.js
CHANGED
|
@@ -90,9 +90,9 @@ function findKernelBin(cwd = process.cwd()) {
|
|
|
90
90
|
const rt = runtimePackageDir()
|
|
91
91
|
const candidates = [
|
|
92
92
|
path.join(cwd, '.owear', 'bin', exe),
|
|
93
|
-
|
|
94
|
-
// monorepo en desarrollo
|
|
93
|
+
// monorepo en desarrollo: el kernel recién compilado manda sobre el paquete npm
|
|
95
94
|
path.resolve(__dirname, '../../../build', platformPreset(), 'src', exe),
|
|
95
|
+
...(rt ? [path.join(rt, 'bin', exe)] : []),
|
|
96
96
|
]
|
|
97
97
|
for (const c of candidates) if (fs.existsSync(c)) return c
|
|
98
98
|
return null
|
|
@@ -171,11 +171,41 @@ function cmdCreate(args) {
|
|
|
171
171
|
die(`template no encontrado en ${templateDir} (instala @owear/cli completo)`)
|
|
172
172
|
}
|
|
173
173
|
fs.cpSync(templateDir, target, { recursive: true })
|
|
174
|
+
applyAppName(target)
|
|
174
175
|
log(`app creada en ${target}`)
|
|
175
176
|
log('siguientes pasos:')
|
|
176
177
|
console.log(` cd ${path.basename(target)}`)
|
|
177
|
-
console.log('
|
|
178
|
-
console.log('
|
|
178
|
+
console.log(' npm install')
|
|
179
|
+
console.log(' npm run dev')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Sustituye los placeholders del template por el nombre de la app
|
|
184
|
+
* (__APP_NAME__ legible, __PKG_NAME__ válido como nombre de paquete npm).
|
|
185
|
+
*/
|
|
186
|
+
function applyAppName(target) {
|
|
187
|
+
const appName = path.basename(target)
|
|
188
|
+
const pkgName =
|
|
189
|
+
appName
|
|
190
|
+
.toLowerCase()
|
|
191
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
192
|
+
.replace(/^[-._]+|[-._]+$/g, '') || 'owear-app'
|
|
193
|
+
const extRe = /\.(ts|tsx|js|mjs|json|html|css|md|txt|ya?ml)$/
|
|
194
|
+
const walk = (dir) => {
|
|
195
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
196
|
+
if (entry.name === 'node_modules') continue
|
|
197
|
+
const p = path.join(dir, entry.name)
|
|
198
|
+
if (entry.isDirectory()) walk(p)
|
|
199
|
+
else if (extRe.test(entry.name)) {
|
|
200
|
+
const src = fs.readFileSync(p, 'utf8')
|
|
201
|
+
const out = src
|
|
202
|
+
.replaceAll('__APP_NAME__', appName)
|
|
203
|
+
.replaceAll('__PKG_NAME__', pkgName)
|
|
204
|
+
if (out !== src) fs.writeFileSync(p, out)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
walk(target)
|
|
179
209
|
}
|
|
180
210
|
|
|
181
211
|
// ── ow api — gestión de las APIs del framework ──────────────────────────────
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<!-- Copyright 2026 Owear Contributors
|
|
2
|
+
SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
# __APP_NAME__
|
|
5
|
+
|
|
6
|
+
App de escritorio construida con **[Owear](https://owear.dev)**: el WebView del
|
|
7
|
+
sistema renderiza tu frontend web y el kernel nativo te da el sistema operativo.
|
|
8
|
+
|
|
9
|
+
## Empezar
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install
|
|
13
|
+
npm run dev # ≡ ow dev
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Se abre una ventana con el WebView del sistema. Edita `src/renderer.ts` y el
|
|
17
|
+
hot-reload de Vite lo refleja al instante.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm run build # ≡ ow build → dist/
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Estructura
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
├── index.html UI (titlebar propia, hero, tarjetas)
|
|
27
|
+
├── src/
|
|
28
|
+
│ ├── renderer.ts lógica del renderer + llamadas a módulos nativos
|
|
29
|
+
│ ├── style.css estilos
|
|
30
|
+
│ └── ow.d.ts tipos de window.ow
|
|
31
|
+
└── app/
|
|
32
|
+
└── main.ts proceso principal (sidecar Node): sólo la ventana
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Cómo se llama al sistema
|
|
36
|
+
|
|
37
|
+
Sin `ipcRenderer` ni `ipcMain`: el renderer invoca los módulos nativos del
|
|
38
|
+
kernel directo.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const path = await ow.invoke('dialog', 'open', 'open', 'Abrir archivo')
|
|
42
|
+
const text = await ow.invoke('fs', 'readText', path)
|
|
43
|
+
await ow.invoke('notification', 'show', 'Listo', `Leídos ${text.length} bytes`)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Módulos incluidos (stock): `fs` · `path` · `process` (PTY) · `dialog` ·
|
|
47
|
+
`clipboard` · `shell` · `screen` · `net` · `notification` · `power` · `menu` ·
|
|
48
|
+
`globalshortcut` · `updater` · `capturer`, más los builtins `ow-window` (ventana),
|
|
49
|
+
`window` (extras del WebView), `session`, `app` y `crashreporter`.
|
|
50
|
+
|
|
51
|
+
## Añadir un módulo nativo propio (C++)
|
|
52
|
+
|
|
53
|
+
1. Crea `native/mi_modulo.cpp`:
|
|
54
|
+
|
|
55
|
+
```cpp
|
|
56
|
+
#include <ow/Json.h>
|
|
57
|
+
#include <ow/Module.h>
|
|
58
|
+
#include "ow_api.h"
|
|
59
|
+
|
|
60
|
+
static void ping(const ow_request_t*, ow_response_t* res) {
|
|
61
|
+
ow::Module::RespondOk(res, "\"pong\"");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
OW_MODULE_BEGIN(mi_modulo, "1.0.0")
|
|
65
|
+
OW_FN(ping)
|
|
66
|
+
OW_MODULE_END()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
2. `ow dev` lo compila a `.owm` y genera el binding. Úsalo en el renderer:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { mi_modulo } from '@owear/native'
|
|
73
|
+
await mi_modulo.ping()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
> Requiere un compilador de C++ en el sistema. Si no lo tienes, no añadas
|
|
77
|
+
> `native/` y la app sigue funcionando con los módulos stock.
|
package/template/app/main.ts
CHANGED
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
// Copyright 2026 Owear Contributors
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
//
|
|
4
|
-
// main.ts — proceso principal (sidecar Node)
|
|
4
|
+
// app/main.ts — proceso principal (sidecar Node), estilo Electron.
|
|
5
5
|
//
|
|
6
|
-
|
|
6
|
+
// Sólo gestiona el ciclo de vida de la ventana. La app en sí vive en el
|
|
7
|
+
// renderer, que llama a los módulos nativos directo. Aquí podrías añadir
|
|
8
|
+
// lógica privilegiada (por ejemplo, exponerla vía un módulo nativo propio).
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
import { app, BrowserWindow } from '@owear/core'
|
|
9
11
|
|
|
10
12
|
app.whenReady().then(() => {
|
|
11
13
|
const win = new BrowserWindow({
|
|
12
|
-
title: '
|
|
13
|
-
width:
|
|
14
|
+
title: '__APP_NAME__',
|
|
15
|
+
width: 1080,
|
|
14
16
|
height: 720,
|
|
15
|
-
titleBarStyle: 'custom', // titlebar
|
|
17
|
+
titleBarStyle: 'custom', // titlebar propia + regiones [data-ow-drag]
|
|
16
18
|
url: process.env.OW_DEV_SERVER_URL ?? 'app://index.html',
|
|
17
19
|
})
|
|
18
|
-
windows++
|
|
19
20
|
|
|
20
|
-
win.on('
|
|
21
|
-
|
|
22
|
-
if (windows === 0) app.quit()
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
// ejemplo: evaluar JS en la página
|
|
26
|
-
win.on('ready-to-show', async () => {
|
|
27
|
-
console.log('[main] ventana lista, id =', win.id)
|
|
28
|
-
})
|
|
21
|
+
win.on('ready-to-show', () => console.log('[main] ventana lista, id =', win.id))
|
|
22
|
+
win.on('closed', () => app.quit())
|
|
29
23
|
})
|
package/template/index.html
CHANGED
|
@@ -1,24 +1,85 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
+
<!-- Copyright 2026 Owear Contributors
|
|
3
|
+
SPDX-License-Identifier: Apache-2.0 -->
|
|
4
|
+
|
|
2
5
|
<html lang="es">
|
|
3
|
-
<head>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
</head>
|
|
8
|
-
<body>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="UTF-8" />
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
9
|
+
<title>__APP_NAME__</title>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<!-- Titlebar propia: el kernel convierte [data-ow-drag] en zona de arrastre.
|
|
13
|
+
Los controles van FUERA de esa zona para no iniciar drag al pulsarlos. -->
|
|
14
|
+
<header class="titlebar">
|
|
15
|
+
<div class="titlebar__drag" data-ow-drag>
|
|
16
|
+
<span class="brand__mark" aria-hidden="true"></span>
|
|
17
|
+
<span class="brand__name">__APP_NAME__</span>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="titlebar__controls">
|
|
20
|
+
<button id="win-min" class="winbtn" title="Minimizar" aria-label="Minimizar">
|
|
21
|
+
<svg viewBox="0 0 12 12" width="12" height="12"><path d="M2 6h8" /></svg>
|
|
22
|
+
</button>
|
|
23
|
+
<button id="win-max" class="winbtn" title="Maximizar" aria-label="Maximizar">
|
|
24
|
+
<svg viewBox="0 0 12 12" width="12" height="12"><rect x="2.5" y="2.5" width="7" height="7" rx="1" /></svg>
|
|
25
|
+
</button>
|
|
26
|
+
<button id="win-close" class="winbtn winbtn--close" title="Cerrar" aria-label="Cerrar">
|
|
27
|
+
<svg viewBox="0 0 12 12" width="12" height="12"><path d="M2.5 2.5l7 7M9.5 2.5l-7 7" /></svg>
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
</header>
|
|
31
|
+
|
|
32
|
+
<main class="page">
|
|
33
|
+
<section class="hero">
|
|
34
|
+
<span class="eyebrow">Owear</span>
|
|
35
|
+
<h1>__APP_NAME__</h1>
|
|
36
|
+
<p class="lead">
|
|
37
|
+
Frontend web, sistema nativo. Estos botones llaman <strong>directo</strong> a los
|
|
38
|
+
módulos del kernel — sin <code>ipcRenderer</code> ni <code>ipcMain</code>.
|
|
39
|
+
</p>
|
|
40
|
+
<div class="actions">
|
|
41
|
+
<button class="btn btn--primary" data-action="open">Abrir archivo…</button>
|
|
42
|
+
<button class="btn" data-action="notify">Notificar</button>
|
|
43
|
+
<button class="btn" data-action="copy">Copiar ruta</button>
|
|
44
|
+
<button class="btn" data-action="reveal">Mostrar en carpeta</button>
|
|
45
|
+
<button class="btn btn--ghost" data-action="about">Acerca de</button>
|
|
46
|
+
</div>
|
|
47
|
+
</section>
|
|
48
|
+
|
|
49
|
+
<section class="grid">
|
|
50
|
+
<article class="card">
|
|
51
|
+
<header class="card__head">
|
|
52
|
+
<h2>Entorno</h2>
|
|
53
|
+
<span class="badge" id="env-window">—</span>
|
|
54
|
+
</header>
|
|
55
|
+
<dl class="kv">
|
|
56
|
+
<div><dt>Plataforma</dt><dd id="env-platform">—</dd></div>
|
|
57
|
+
<div><dt>Motor</dt><dd id="env-engine">—</dd></div>
|
|
58
|
+
<div><dt>Escala</dt><dd id="env-dpr">—</dd></div>
|
|
59
|
+
</dl>
|
|
60
|
+
<p class="ua" id="env-ua">—</p>
|
|
61
|
+
</article>
|
|
62
|
+
|
|
63
|
+
<article class="card card--grow">
|
|
64
|
+
<header class="card__head">
|
|
65
|
+
<h2>Consola</h2>
|
|
66
|
+
<button class="linkbtn" data-action="clear">Limpiar</button>
|
|
67
|
+
</header>
|
|
68
|
+
<pre id="console" class="console">Listo. Pulsa «Abrir archivo…» para invocar un módulo nativo.</pre>
|
|
69
|
+
</article>
|
|
70
|
+
</section>
|
|
71
|
+
|
|
72
|
+
<section class="card">
|
|
73
|
+
<header class="card__head"><h2>Cómo funciona</h2></header>
|
|
74
|
+
<p class="hint">
|
|
75
|
+
Cada botón invoca un módulo nativo con <code>ow.invoke()</code>.
|
|
76
|
+
</p>
|
|
77
|
+
<pre class="code"><code>const path = await ow.invoke('dialog', 'open', 'open', 'Abrir archivo')
|
|
78
|
+
const text = await ow.invoke('fs', 'readText', path)
|
|
79
|
+
await ow.invoke('notification', 'show', 'Listo', 'Leídos ' + text.length + ' bytes')</code></pre>
|
|
80
|
+
</section>
|
|
81
|
+
</main>
|
|
82
|
+
|
|
83
|
+
<script type="module" src="/src/renderer.ts"></script>
|
|
84
|
+
</body>
|
|
24
85
|
</html>
|
package/template/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "__PKG_NAME__",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
|
+
"description": "App de escritorio construida con Owear.",
|
|
6
7
|
"scripts": {
|
|
7
8
|
"dev": "ow dev",
|
|
8
|
-
"build": "ow build"
|
|
9
|
+
"build": "ow build",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
9
11
|
},
|
|
10
12
|
"dependencies": {
|
|
11
13
|
"@owear/core": "^0.1.0"
|
|
@@ -13,6 +15,7 @@
|
|
|
13
15
|
"devDependencies": {
|
|
14
16
|
"@owear/cli": "^0.1.0",
|
|
15
17
|
"@owear/vite-plugin": "^0.1.0",
|
|
18
|
+
"@types/node": "^20.14.0",
|
|
16
19
|
"esbuild": "^0.25.0",
|
|
17
20
|
"typescript": "^5.6.0",
|
|
18
21
|
"vite": "^6.0.0"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Copyright 2026 Owear Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// Tipos de la API que el kernel inyecta en cada documento (`window.ow`).
|
|
5
|
+
// Mantenidos a mano contra docs/API.md §1.
|
|
6
|
+
|
|
7
|
+
export {}
|
|
8
|
+
|
|
9
|
+
declare global {
|
|
10
|
+
interface Window {
|
|
11
|
+
__owWindowId: number
|
|
12
|
+
ow: {
|
|
13
|
+
invoke(module: string, fn: string, ...args: unknown[]): Promise<unknown>
|
|
14
|
+
invokeSync(module: string, fn: string, ...args: unknown[]): unknown
|
|
15
|
+
readShared(handle: { id: string; size: number }): Promise<ArrayBuffer>
|
|
16
|
+
on(name: string, cb: (payload: unknown) => void): () => void
|
|
17
|
+
emit(name: string, payload?: unknown): void
|
|
18
|
+
emitTo(targetWindowId: number, name: string, payload?: unknown): void
|
|
19
|
+
findInPage(
|
|
20
|
+
text: string,
|
|
21
|
+
opts?: { matchCase?: boolean; backwards?: boolean }
|
|
22
|
+
): { matches: number; active: number }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
package/template/src/renderer.ts
CHANGED
|
@@ -1,30 +1,144 @@
|
|
|
1
1
|
// Copyright 2026 Owear Contributors
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
//
|
|
4
|
-
// renderer.ts — corre dentro del WebView. `ow` lo inyecta el kernel.
|
|
4
|
+
// src/renderer.ts — corre dentro del WebView. `ow` lo inyecta el kernel.
|
|
5
5
|
//
|
|
6
|
-
|
|
6
|
+
// Aquí no hay `ipcRenderer` ni handlers en el main: el renderer llama a los
|
|
7
|
+
// módulos nativos del framework DIRECTO (mismo salto WebView → kernel).
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
const demo = document.getElementById('demo')!
|
|
9
|
+
import './style.css'
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
// módulo nativo PROPIO (native/files.cpp)
|
|
14
|
-
const text = await files.readText('/etc/hostname')
|
|
15
|
-
demo.textContent = `hostname leído desde C++ nativo: ${text.trim()}`
|
|
11
|
+
const APP = document.title || 'Owear App'
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
out.textContent = JSON.stringify(st, null, 2)
|
|
13
|
+
const $ = <T extends HTMLElement = HTMLElement>(sel: string): T =>
|
|
14
|
+
document.querySelector(sel) as T
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
const invoke = <T = unknown>(module: string, fn: string, ...args: unknown[]): Promise<T> =>
|
|
17
|
+
window.ow.invoke(module, fn, ...args) as Promise<T>
|
|
18
|
+
|
|
19
|
+
let lastPath: string | null = null
|
|
20
|
+
|
|
21
|
+
// ── Consola ─────────────────────────────────────────────────────────────
|
|
22
|
+
const consoleEl = $('#console')
|
|
23
|
+
|
|
24
|
+
function log(msg: string, kind: 'ok' | 'err' | 'muted' = 'muted'): void {
|
|
25
|
+
const time = new Date().toLocaleTimeString()
|
|
26
|
+
const line = document.createElement('span')
|
|
27
|
+
line.className = `line--${kind}`
|
|
28
|
+
line.textContent = `[${time}] ${msg}\n`
|
|
29
|
+
consoleEl.appendChild(line)
|
|
30
|
+
consoleEl.scrollTop = consoleEl.scrollHeight
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function logError(err: unknown): void {
|
|
34
|
+
log(err instanceof Error ? err.message : String(err), 'err')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Entorno ─────────────────────────────────────────────────────────────
|
|
38
|
+
function renderEnvironment(): void {
|
|
39
|
+
const ua = navigator.userAgent
|
|
40
|
+
const engine = /AppleWebKit\/([\d.]+)/.exec(ua)?.[1]
|
|
41
|
+
$('#env-platform').textContent = navigator.platform || '—'
|
|
42
|
+
$('#env-engine').textContent = engine ? `WebKit ${engine}` : '—'
|
|
43
|
+
$('#env-dpr').textContent = `${window.devicePixelRatio}×`
|
|
44
|
+
$('#env-window').textContent = `#${window.__owWindowId}`
|
|
45
|
+
$('#env-ua').textContent = ua
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Acciones (módulos nativos) ──────────────────────────────────────────
|
|
49
|
+
async function openFile(): Promise<void> {
|
|
50
|
+
const path = await invoke<string | null>('dialog', 'open', 'open', 'Abrir un archivo')
|
|
51
|
+
if (!path) {
|
|
52
|
+
log('dialog.open → cancelado')
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
lastPath = path
|
|
56
|
+
const stat = await invoke<{ size: number }>('fs', 'stat', path)
|
|
57
|
+
const text = await invoke<string>('fs', 'readText', path)
|
|
58
|
+
log(`dialog.open → ${path}`, 'ok')
|
|
59
|
+
log(`fs.readText → ${stat.size} bytes`, 'ok')
|
|
60
|
+
|
|
61
|
+
const clean = text.replace(/\u0000/g, '')
|
|
62
|
+
const preview = clean.length > 6000 ? `${clean.slice(0, 6000)}\n… (vista truncada)` : clean
|
|
63
|
+
consoleEl.textContent = ''
|
|
64
|
+
log(`── ${path} (${stat.size} bytes) ──`, 'muted')
|
|
65
|
+
const body = document.createElement('span')
|
|
66
|
+
body.style.color = '#c8d4e0'
|
|
67
|
+
body.textContent = preview + '\n'
|
|
68
|
+
consoleEl.appendChild(body)
|
|
69
|
+
consoleEl.scrollTop = 0
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function notifyUser(): Promise<void> {
|
|
73
|
+
await invoke('notification', 'show', APP, 'Los módulos nativos funcionan sin IPC.', APP)
|
|
74
|
+
log('notification.show → notificación enviada', 'ok')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function copyPath(): Promise<void> {
|
|
78
|
+
if (!lastPath) {
|
|
79
|
+
log('Abre un archivo antes de copiar.', 'err')
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
await invoke('clipboard', 'writeText', lastPath)
|
|
83
|
+
log(`clipboard.writeText → ${lastPath}`, 'ok')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function revealPath(): Promise<void> {
|
|
87
|
+
if (!lastPath) {
|
|
88
|
+
log('Abre un archivo antes de mostrarlo.', 'err')
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
await invoke('shell', 'showItemInFolder', lastPath)
|
|
92
|
+
log(`shell.showItemInFolder → ${lastPath}`, 'ok')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function about(): Promise<void> {
|
|
96
|
+
await invoke('dialog', 'messageBox', 'info', `Acerca de ${APP}`, `${APP}\n\nConstruido con Owear.`, [
|
|
97
|
+
'Cerrar',
|
|
98
|
+
])
|
|
99
|
+
log('dialog.messageBox → cerrado', 'ok')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Titlebar (builtin ow-window) ────────────────────────────────────────
|
|
103
|
+
function wireTitlebar(): void {
|
|
104
|
+
const id = window.__owWindowId
|
|
105
|
+
$('#win-min').addEventListener('click', () => void invoke('ow-window', 'minimize', id))
|
|
106
|
+
$('#win-max').addEventListener('click', async () => {
|
|
107
|
+
const max = await invoke<boolean>('ow-window', 'isMaximized', id)
|
|
108
|
+
void invoke('ow-window', 'maximize', id, !max)
|
|
109
|
+
})
|
|
110
|
+
$('#win-close').addEventListener('click', () => void invoke('ow-window', 'close', id))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Arranque ────────────────────────────────────────────────────────────
|
|
114
|
+
const actions: Record<string, () => void | Promise<void>> = {
|
|
115
|
+
open: openFile,
|
|
116
|
+
notify: notifyUser,
|
|
117
|
+
copy: copyPath,
|
|
118
|
+
reveal: revealPath,
|
|
119
|
+
about,
|
|
120
|
+
clear: () => {
|
|
121
|
+
consoleEl.textContent = ''
|
|
122
|
+
log('Consola limpia.')
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function boot(): void {
|
|
127
|
+
wireTitlebar()
|
|
128
|
+
renderEnvironment()
|
|
129
|
+
|
|
130
|
+
for (const btn of document.querySelectorAll<HTMLElement>('[data-action]')) {
|
|
131
|
+
const action = actions[btn.dataset.action ?? '']
|
|
132
|
+
if (!action) continue
|
|
133
|
+
btn.addEventListener('click', () => {
|
|
134
|
+
Promise.resolve(action()).catch(logError)
|
|
24
135
|
})
|
|
25
|
-
} catch (e) {
|
|
26
|
-
demo.textContent = '✗ ' + (e as Error).message
|
|
27
136
|
}
|
|
137
|
+
|
|
138
|
+
window.addEventListener('resize', renderEnvironment)
|
|
139
|
+
window.ow.on('resize', renderEnvironment)
|
|
140
|
+
|
|
141
|
+
log(`Renderer listo. Ventana ${window.__owWindowId}.`, 'ok')
|
|
28
142
|
}
|
|
29
143
|
|
|
30
|
-
|
|
144
|
+
boot()
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/* Copyright 2026 Owear Contributors
|
|
2
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
color-scheme: dark;
|
|
7
|
+
--bg: #0d1117;
|
|
8
|
+
--panel: #161b22;
|
|
9
|
+
--panel-2: #0f141a;
|
|
10
|
+
--line: #262d38;
|
|
11
|
+
--line-2: #30363d;
|
|
12
|
+
--text: #e6edf3;
|
|
13
|
+
--muted: #8b949e;
|
|
14
|
+
--accent: #3b82f6;
|
|
15
|
+
--accent-hover: #2f6fd0;
|
|
16
|
+
--danger: #e5484d;
|
|
17
|
+
--ok: #3fb950;
|
|
18
|
+
--radius: 12px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
* {
|
|
22
|
+
box-sizing: border-box;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
html,
|
|
26
|
+
body {
|
|
27
|
+
height: 100%;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
body {
|
|
31
|
+
margin: 0;
|
|
32
|
+
background: var(--bg);
|
|
33
|
+
color: var(--text);
|
|
34
|
+
font: 14px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
user-select: none;
|
|
39
|
+
-webkit-user-select: none;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* ── Titlebar ─────────────────────────────────────────────────────────── */
|
|
43
|
+
.titlebar {
|
|
44
|
+
height: 40px;
|
|
45
|
+
flex: 0 0 auto;
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: stretch;
|
|
48
|
+
background: #010409;
|
|
49
|
+
border-bottom: 1px solid var(--line);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.titlebar__drag {
|
|
53
|
+
flex: 1;
|
|
54
|
+
min-width: 0;
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
gap: 10px;
|
|
58
|
+
padding: 0 14px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.brand__mark {
|
|
62
|
+
width: 12px;
|
|
63
|
+
height: 12px;
|
|
64
|
+
border-radius: 3px;
|
|
65
|
+
background: var(--accent);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.brand__name {
|
|
69
|
+
font-weight: 600;
|
|
70
|
+
letter-spacing: 0.2px;
|
|
71
|
+
white-space: nowrap;
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
text-overflow: ellipsis;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.titlebar__controls {
|
|
77
|
+
display: flex;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.winbtn {
|
|
81
|
+
width: 46px;
|
|
82
|
+
border: 0;
|
|
83
|
+
background: transparent;
|
|
84
|
+
color: var(--muted);
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
display: grid;
|
|
87
|
+
place-items: center;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.winbtn svg {
|
|
91
|
+
fill: none;
|
|
92
|
+
stroke: currentColor;
|
|
93
|
+
stroke-width: 1.4;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.winbtn:hover {
|
|
97
|
+
background: var(--line);
|
|
98
|
+
color: var(--text);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.winbtn--close:hover {
|
|
102
|
+
background: var(--danger);
|
|
103
|
+
color: #fff;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ── Layout ───────────────────────────────────────────────────────────── */
|
|
107
|
+
.page {
|
|
108
|
+
flex: 1;
|
|
109
|
+
min-height: 0;
|
|
110
|
+
overflow: auto;
|
|
111
|
+
padding: 28px clamp(18px, 4vw, 44px) 36px;
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
gap: 20px;
|
|
115
|
+
max-width: 1040px;
|
|
116
|
+
width: 100%;
|
|
117
|
+
margin: 0 auto;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.hero {
|
|
121
|
+
display: flex;
|
|
122
|
+
flex-direction: column;
|
|
123
|
+
gap: 12px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.eyebrow {
|
|
127
|
+
font-size: 12px;
|
|
128
|
+
font-weight: 600;
|
|
129
|
+
letter-spacing: 0.08em;
|
|
130
|
+
text-transform: uppercase;
|
|
131
|
+
color: var(--muted);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
h1 {
|
|
135
|
+
margin: 0;
|
|
136
|
+
font-size: clamp(24px, 3vw, 32px);
|
|
137
|
+
line-height: 1.2;
|
|
138
|
+
letter-spacing: -0.01em;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.lead {
|
|
142
|
+
margin: 0;
|
|
143
|
+
max-width: 72ch;
|
|
144
|
+
color: var(--muted);
|
|
145
|
+
font-size: 15px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.lead strong {
|
|
149
|
+
color: var(--text);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.actions {
|
|
153
|
+
display: flex;
|
|
154
|
+
flex-wrap: wrap;
|
|
155
|
+
gap: 8px;
|
|
156
|
+
margin-top: 4px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.btn {
|
|
160
|
+
font: inherit;
|
|
161
|
+
color: var(--text);
|
|
162
|
+
background: #21262d;
|
|
163
|
+
border: 1px solid var(--line-2);
|
|
164
|
+
border-radius: 8px;
|
|
165
|
+
padding: 8px 14px;
|
|
166
|
+
cursor: pointer;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.btn:hover {
|
|
170
|
+
background: #2b323b;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.btn:active {
|
|
174
|
+
transform: translateY(1px);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.btn--primary {
|
|
178
|
+
background: var(--accent);
|
|
179
|
+
border-color: var(--accent);
|
|
180
|
+
color: #fff;
|
|
181
|
+
font-weight: 600;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.btn--primary:hover {
|
|
185
|
+
background: var(--accent-hover);
|
|
186
|
+
border-color: var(--accent-hover);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.btn--ghost {
|
|
190
|
+
background: transparent;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.grid {
|
|
194
|
+
display: grid;
|
|
195
|
+
grid-template-columns: minmax(240px, 300px) 1fr;
|
|
196
|
+
gap: 16px;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@media (max-width: 820px) {
|
|
200
|
+
.grid {
|
|
201
|
+
grid-template-columns: 1fr;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.card {
|
|
206
|
+
background: var(--panel);
|
|
207
|
+
border: 1px solid var(--line);
|
|
208
|
+
border-radius: var(--radius);
|
|
209
|
+
padding: 16px 18px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.card--grow {
|
|
213
|
+
min-width: 0;
|
|
214
|
+
display: flex;
|
|
215
|
+
flex-direction: column;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.card__head {
|
|
219
|
+
display: flex;
|
|
220
|
+
align-items: center;
|
|
221
|
+
justify-content: space-between;
|
|
222
|
+
margin-bottom: 12px;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.card__head h2 {
|
|
226
|
+
margin: 0;
|
|
227
|
+
font-size: 12px;
|
|
228
|
+
text-transform: uppercase;
|
|
229
|
+
letter-spacing: 0.07em;
|
|
230
|
+
color: var(--muted);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.badge {
|
|
234
|
+
font-size: 12px;
|
|
235
|
+
color: var(--muted);
|
|
236
|
+
background: var(--panel-2);
|
|
237
|
+
border: 1px solid var(--line);
|
|
238
|
+
border-radius: 999px;
|
|
239
|
+
padding: 2px 10px;
|
|
240
|
+
font-variant-numeric: tabular-nums;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.kv {
|
|
244
|
+
margin: 0;
|
|
245
|
+
display: flex;
|
|
246
|
+
flex-direction: column;
|
|
247
|
+
gap: 8px;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.kv > div {
|
|
251
|
+
display: flex;
|
|
252
|
+
justify-content: space-between;
|
|
253
|
+
gap: 12px;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.kv dt {
|
|
257
|
+
color: var(--muted);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.kv dd {
|
|
261
|
+
margin: 0;
|
|
262
|
+
font-variant-numeric: tabular-nums;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.ua {
|
|
266
|
+
margin: 14px 0 0;
|
|
267
|
+
color: #5b6672;
|
|
268
|
+
font-size: 12px;
|
|
269
|
+
word-break: break-word;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.console {
|
|
273
|
+
flex: 1;
|
|
274
|
+
min-height: 210px;
|
|
275
|
+
max-height: 46vh;
|
|
276
|
+
margin: 0;
|
|
277
|
+
overflow: auto;
|
|
278
|
+
background: #010409;
|
|
279
|
+
border: 1px solid var(--line);
|
|
280
|
+
border-radius: 8px;
|
|
281
|
+
padding: 12px 14px;
|
|
282
|
+
font: 12.5px/1.6 ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
|
283
|
+
white-space: pre-wrap;
|
|
284
|
+
word-break: break-word;
|
|
285
|
+
user-select: text;
|
|
286
|
+
-webkit-user-select: text;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.console .line--ok {
|
|
290
|
+
color: var(--ok);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.console .line--err {
|
|
294
|
+
color: #ff8085;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.console .line--muted {
|
|
298
|
+
color: var(--muted);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.linkbtn {
|
|
302
|
+
font: inherit;
|
|
303
|
+
background: transparent;
|
|
304
|
+
border: 0;
|
|
305
|
+
color: var(--muted);
|
|
306
|
+
cursor: pointer;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.linkbtn:hover {
|
|
310
|
+
color: var(--text);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.hint {
|
|
314
|
+
margin: 0 0 12px;
|
|
315
|
+
color: var(--muted);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.code {
|
|
319
|
+
margin: 0;
|
|
320
|
+
overflow: auto;
|
|
321
|
+
background: #010409;
|
|
322
|
+
border: 1px solid var(--line);
|
|
323
|
+
border-radius: 8px;
|
|
324
|
+
padding: 14px;
|
|
325
|
+
font: 12.5px/1.7 ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
|
326
|
+
color: #c9d1d9;
|
|
327
|
+
user-select: text;
|
|
328
|
+
-webkit-user-select: text;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
code {
|
|
332
|
+
color: #79c0ff;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.hint code,
|
|
336
|
+
.lead code {
|
|
337
|
+
background: #21262d;
|
|
338
|
+
border: 1px solid var(--line);
|
|
339
|
+
border-radius: 6px;
|
|
340
|
+
padding: 1px 6px;
|
|
341
|
+
font-size: 12.5px;
|
|
342
|
+
}
|
package/template/tsconfig.json
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
"target": "ES2022",
|
|
4
4
|
"module": "ESNext",
|
|
5
5
|
"moduleResolution": "Bundler",
|
|
6
|
-
"lib": ["ES2022", "DOM"],
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"types": ["node", "vite/client"],
|
|
7
8
|
"strict": true,
|
|
8
9
|
"skipLibCheck": true,
|
|
9
|
-
"
|
|
10
|
+
"noEmit": true
|
|
10
11
|
},
|
|
11
12
|
"include": ["src", "app"]
|
|
12
13
|
}
|
package/template/vite.config.ts
CHANGED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 Owear Contributors
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
//
|
|
4
|
-
// native/files.cpp — módulo nativo PROPIO de la app.
|
|
5
|
-
// Se compila a .owm y se invoca desde el renderer con:
|
|
6
|
-
// import { files } from '@owear/native'
|
|
7
|
-
// await files.readText('/etc/hostname')
|
|
8
|
-
//
|
|
9
|
-
#include <ow/Json.h>
|
|
10
|
-
#include <ow/Module.h>
|
|
11
|
-
#include "ow_api.h"
|
|
12
|
-
|
|
13
|
-
#include <filesystem>
|
|
14
|
-
#include <fstream>
|
|
15
|
-
#include <sstream>
|
|
16
|
-
|
|
17
|
-
namespace fs = std::filesystem;
|
|
18
|
-
|
|
19
|
-
static void readText(const ow_request_t* req, ow_response_t* res) {
|
|
20
|
-
auto parsed = ow::json::Parse(std::string_view(req->json, req->json_len));
|
|
21
|
-
if (!parsed.value || !parsed.value->IsArray() || !parsed.value->AsArray()[0].IsString())
|
|
22
|
-
return ow::Module::RespondError(res, "readText(path): path requerido");
|
|
23
|
-
std::ifstream f(parsed.value->AsArray()[0].AsString(), std::ios::binary);
|
|
24
|
-
if (!f) return ow::Module::RespondError(res, "no se pudo abrir");
|
|
25
|
-
std::ostringstream ss;
|
|
26
|
-
ss << f.rdbuf();
|
|
27
|
-
ow::Module::RespondOk(res, ow::json::Value(ss.str()).Serialize().c_str());
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
static void exists(const ow_request_t* req, ow_response_t* res) {
|
|
31
|
-
auto parsed = ow::json::Parse(std::string_view(req->json, req->json_len));
|
|
32
|
-
if (!parsed.value || !parsed.value->IsArray() || !parsed.value->AsArray()[0].IsString())
|
|
33
|
-
return ow::Module::RespondError(res, "exists(path): path requerido");
|
|
34
|
-
std::error_code ec;
|
|
35
|
-
bool ok = fs::exists(parsed.value->AsArray()[0].AsString(), ec);
|
|
36
|
-
ow::Module::RespondOk(res, ok ? "true" : "false");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
OW_MODULE_BEGIN(files, "1.0.0")
|
|
40
|
-
OW_FN(readText)
|
|
41
|
-
OW_FN(exists)
|
|
42
|
-
OW_MODULE_END()
|