node-gtk 2.1.0 → 3.0.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 +52 -152
- package/bin/node-gtk.js +12 -1
- package/lib/esm/hooks.mjs +49 -0
- package/lib/esm/register.mjs +17 -0
- package/lib/index.js +1 -2
- package/lib/index.mjs +25 -0
- package/lib/inspect.js +1 -1
- package/lib/loop.js +5 -0
- package/lib/module.js +8 -2
- package/lib/native.js +51 -0
- package/lib/register-class.js +86 -3
- package/lib/styles.d.ts +81 -0
- package/lib/styles.js +428 -0
- package/package.json +15 -1
- package/scripts/windows-bundle-runtime.sh +163 -0
- package/scripts/windows-smoke-test.js +104 -0
- package/src/closure.cc +19 -6
- package/src/closure.h +8 -4
- package/src/function.cc +47 -0
- package/src/gi.cc +10 -0
- package/src/gobject.cc +170 -3
- package/src/gobject.h +3 -0
- package/tools/README.md +52 -2
- package/tools/create-app.js +246 -0
- package/tools/generate-types.js +80 -3
- package/tools/list-libraries.js +125 -0
- package/tools/templates/app/README.md.tmpl +97 -0
- package/tools/templates/app/gitignore.tmpl +10 -0
- package/tools/templates/app/package.json.tmpl +26 -0
- package/tools/templates/app/src/main.ts.tmpl +110 -0
- package/tools/templates/app/src/welcome.ts.tmpl +41 -0
- package/tools/templates/app/style.css.tmpl +19 -0
- package/tools/templates/app/tsconfig.json.tmpl +19 -0
- /package/{COPYING → LICENSE} +0 -0
package/lib/styles.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Type declarations for node-gtk/styles (see lib/styles.js).
|
|
3
|
+
*
|
|
4
|
+
* The public surface deals only in strings, URLs, and plain handles, so these
|
|
5
|
+
* types are self-contained — they don't reference the GTK typings.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Options shared by the style sheets. */
|
|
9
|
+
export interface StyleOptions {
|
|
10
|
+
/** Provider priority; defaults to `Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION`. */
|
|
11
|
+
priority?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Options for {@link StyleManager.add}. */
|
|
15
|
+
export interface StyleAddOptions extends StyleOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Watch the calling module for hot-reload (defaults to on in development).
|
|
18
|
+
* Pass `false` for a programmatic sheet whose source can't be re-imported
|
|
19
|
+
* safely (built inside a method, or owned by a stateful module); the handle
|
|
20
|
+
* still supports `update`/`refresh`/`remove`.
|
|
21
|
+
*/
|
|
22
|
+
watch?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Options for {@link StyleManager.addFile}. */
|
|
26
|
+
export interface StyleFileOptions extends StyleOptions {
|
|
27
|
+
/** Watch the file for hot-reload (defaults to on in development). */
|
|
28
|
+
watch?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A handle to an installed (or queued) stylesheet. */
|
|
32
|
+
export interface StyleSheet {
|
|
33
|
+
/**
|
|
34
|
+
* Replace the CSS (or, for a file sheet, re-read the path) in place. For an
|
|
35
|
+
* inline sheet a string replaces any render function — it becomes fixed CSS.
|
|
36
|
+
*/
|
|
37
|
+
update(next: string): void
|
|
38
|
+
/**
|
|
39
|
+
* Re-apply the sheet from its current source: re-run the render function
|
|
40
|
+
* (inline) or re-read the path (file). Call it when the state a render reads
|
|
41
|
+
* changes. A no-op for a queued sheet (not yet installed).
|
|
42
|
+
*/
|
|
43
|
+
refresh(): void
|
|
44
|
+
/** Remove the sheet from the display. */
|
|
45
|
+
remove(): void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Applies CSS to a GTK app and, in development, hot-reloads it. The module
|
|
50
|
+
* exports a shared {@link styles} instance; constructing your own is rarely
|
|
51
|
+
* needed.
|
|
52
|
+
*/
|
|
53
|
+
export declare class StyleManager {
|
|
54
|
+
/**
|
|
55
|
+
* Queue (or, once the display exists, install) inline CSS. The source file it
|
|
56
|
+
* is called from is watched for hot-reload (unless `watch` is false).
|
|
57
|
+
*
|
|
58
|
+
* `css` may be a `() => string` *render* function instead of a string, for a
|
|
59
|
+
* dynamic stylesheet built from live state (theme, fonts, …). The render runs
|
|
60
|
+
* now and again on every hot-reload of its module, and on demand via the
|
|
61
|
+
* handle's {@link StyleSheet.refresh}. Keep such a module side-effect-free at
|
|
62
|
+
* its top level (a re-import re-runs it).
|
|
63
|
+
*/
|
|
64
|
+
add(css: string | (() => string), options?: StyleAddOptions): StyleSheet
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Queue (or install) a `.css` file. Unless `watch` is false, the file is
|
|
68
|
+
* watched and re-read into its provider on every edit. Idempotent per path.
|
|
69
|
+
* @param path A path, a `file://` URL string, or a `URL`.
|
|
70
|
+
*/
|
|
71
|
+
addFile(path: string | URL, options?: StyleFileOptions): StyleSheet
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Install everything queued before the display existed, and start the file
|
|
75
|
+
* watcher. Call once from your `activate` handler. Safe to call repeatedly.
|
|
76
|
+
*/
|
|
77
|
+
install(): void
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The application's shared StyleManager. */
|
|
81
|
+
export declare const styles: StyleManager
|
package/lib/styles.js
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* styles.js — a small StyleManager for node-gtk apps. Applies CSS (inline via
|
|
3
|
+
* styles.add, a `.css` file via styles.addFile) and hot-reloads it in
|
|
4
|
+
* development. Adapted from zym's style-manager; see doc/styles.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const Path = require('node:path')
|
|
8
|
+
const { pathToFileURL, fileURLToPath } = require('node:url')
|
|
9
|
+
|
|
10
|
+
const internal = require('./native.js')
|
|
11
|
+
const Module = require('./module.js')
|
|
12
|
+
|
|
13
|
+
// Lazily resolved: the app loads Gtk/Gdk/GLib/Gio, not this module. GLib/Gio
|
|
14
|
+
// drive hot-reload (watchDir) and arrive with Gtk.
|
|
15
|
+
const moduleCache = internal.GetModuleCache()
|
|
16
|
+
const gtk = () => Module.require('Gtk')
|
|
17
|
+
const gdk = () => Module.require('Gdk')
|
|
18
|
+
const glib = () => Module.require('GLib')
|
|
19
|
+
const gio = () => Module.require('Gio')
|
|
20
|
+
|
|
21
|
+
// Dev-only; opt out with a falsy NODE_GTK_STYLE_HOT_RELOAD (0/false/no/off).
|
|
22
|
+
const HOT_RELOAD =
|
|
23
|
+
process.env.NODE_ENV === 'development' &&
|
|
24
|
+
!/^(0|false|no|off)$/i.test(process.env.NODE_GTK_STYLE_HOT_RELOAD ?? '')
|
|
25
|
+
|
|
26
|
+
const OWN_FILE = __filename // skipped during caller detection
|
|
27
|
+
|
|
28
|
+
const DEBOUNCE_MS = 40 // coalesce the burst an editor's atomic save emits
|
|
29
|
+
|
|
30
|
+
function defaultPriority() {
|
|
31
|
+
return gtk().STYLE_PROVIDER_PRIORITY_APPLICATION ?? 600
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The default display, or null before it exists (e.g. before app activation). */
|
|
35
|
+
function defaultDisplay() {
|
|
36
|
+
if (!moduleCache['Gdk']) return null // don't force-load Gdk just to ask
|
|
37
|
+
return gdk().Display.getDefault()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Normalise a path / `file:` URL string / URL to an absolute path. */
|
|
41
|
+
function toPath(p) {
|
|
42
|
+
if (p instanceof URL) return fileURLToPath(p)
|
|
43
|
+
if (typeof p === 'string' && p.startsWith('file:')) return fileURLToPath(p)
|
|
44
|
+
return Path.resolve(p)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function loadCss(provider, css) {
|
|
48
|
+
// loadFromString is GTK 4.12+; fall back on older 4.x.
|
|
49
|
+
if (typeof provider.loadFromString === 'function') provider.loadFromString(css)
|
|
50
|
+
else provider.loadFromData(css)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Local path of a GFile from a monitor event, or null. GFile is a GInterface,
|
|
54
|
+
* so its methods live on the prototype, not the instance. */
|
|
55
|
+
function gioPath(file) {
|
|
56
|
+
if (!file) return null
|
|
57
|
+
try { return gio().File.prototype.getPath.call(file) } catch { return null }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Absolute path of the first source file above this module on the stack. */
|
|
61
|
+
function callerFile() {
|
|
62
|
+
const stack = new Error().stack
|
|
63
|
+
if (!stack) return null
|
|
64
|
+
for (const line of stack.split('\n').slice(1)) {
|
|
65
|
+
const file = frameFile(line)
|
|
66
|
+
if (file && file !== OWN_FILE) return file
|
|
67
|
+
}
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Pull the absolute file path out of one V8 stack frame, or null. */
|
|
72
|
+
function frameFile(frame) {
|
|
73
|
+
// Matches "(file:line:col)", bare "at file:line:col", and the "at async
|
|
74
|
+
// file:..." form V8 uses for top-level ESM code.
|
|
75
|
+
const m = frame.match(/\(([^()]+):\d+:\d+\)\s*$/) || frame.match(/\bat\s+(?:async\s+)?([^()\s]+):\d+:\d+\s*$/)
|
|
76
|
+
if (!m) return null
|
|
77
|
+
let loc = m[1]
|
|
78
|
+
const q = loc.indexOf('?') // drop the cache-buster query, if any
|
|
79
|
+
if (q !== -1) loc = loc.slice(0, q)
|
|
80
|
+
if (loc.startsWith('file://')) {
|
|
81
|
+
try { return Path.resolve(fileURLToPath(loc)) } catch { return null }
|
|
82
|
+
}
|
|
83
|
+
if (loc.startsWith('node:') || loc.includes('node_modules')) return null
|
|
84
|
+
return Path.resolve(loc)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A handle to an installed (or queued) stylesheet.
|
|
89
|
+
* @typedef {object} StyleSheet
|
|
90
|
+
* @property {(next: string) => void} update Replace with fixed CSS (drops any render fn).
|
|
91
|
+
* @property {() => void} refresh Re-apply from source (re-run render / re-read file).
|
|
92
|
+
* @property {() => void} remove Remove the sheet from the display.
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
class StyleManager {
|
|
96
|
+
constructor() {
|
|
97
|
+
this.ready = false
|
|
98
|
+
this.queued = [] // entries awaiting the display
|
|
99
|
+
|
|
100
|
+
// File sheets by path, for per-path dedup and reload. path -> Set<entry>.
|
|
101
|
+
this.cssEntries = new Map()
|
|
102
|
+
|
|
103
|
+
// ---- Hot-reload state (only used when HOT_RELOAD is on) ----
|
|
104
|
+
this.fileProviders = new Map() // srcFile -> Set<provider>, so a reload drops the old
|
|
105
|
+
this.watchedFiles = new Set()
|
|
106
|
+
this.dirWatchers = new Map() // dir -> Gio.FileMonitor
|
|
107
|
+
this.reloadSeq = 0
|
|
108
|
+
this.reloadTimers = new Map()
|
|
109
|
+
this.reloading = new Set()
|
|
110
|
+
this.reloadPending = new Set()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Inline CSS, hot-reloaded by re-importing its source module. Queued until the
|
|
115
|
+
* display exists.
|
|
116
|
+
*
|
|
117
|
+
* `css` may be a `() => string` *render* function instead of a string, for
|
|
118
|
+
* dynamic stylesheets built from live state (theme, fonts, …). The render runs
|
|
119
|
+
* now and again on every hot-reload of its module (so editing the
|
|
120
|
+
* CSS-generating code re-applies it), and on demand via the handle's
|
|
121
|
+
* `refresh()` — call it whenever the state it reads changes. Keep such a module
|
|
122
|
+
* side-effect-free at its top level (a re-import re-runs it); a stateful
|
|
123
|
+
* singleton it owns must survive re-import (e.g. guarded on `globalThis`).
|
|
124
|
+
*
|
|
125
|
+
* Pass `watch: false` to install without watching the caller for hot-reload —
|
|
126
|
+
* for programmatic sheets whose source can't be re-imported safely (created
|
|
127
|
+
* inside a method, owned by a stateful module). The handle still supports
|
|
128
|
+
* `update`/`refresh`/`remove`.
|
|
129
|
+
* @param {string | (() => string)} css Inline CSS, or a render function.
|
|
130
|
+
* @param {{ priority?: number, watch?: boolean }} [options]
|
|
131
|
+
* @returns {StyleSheet}
|
|
132
|
+
*/
|
|
133
|
+
add(css, options = {}) {
|
|
134
|
+
const render = typeof css === 'function' ? css : null
|
|
135
|
+
const watch = HOT_RELOAD && (options.watch ?? true)
|
|
136
|
+
const file = watch ? callerFile() : null
|
|
137
|
+
const entry = {
|
|
138
|
+
kind: 'inline',
|
|
139
|
+
render,
|
|
140
|
+
css: render ? null : css,
|
|
141
|
+
file,
|
|
142
|
+
priority: options.priority,
|
|
143
|
+
provider: null,
|
|
144
|
+
cancelled: false,
|
|
145
|
+
}
|
|
146
|
+
this.place(entry)
|
|
147
|
+
if (file) this.watch(file)
|
|
148
|
+
return this.handle(entry)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A `.css` file, hot-reloaded by re-reading it (unless `watch` is false).
|
|
153
|
+
* Idempotent per path.
|
|
154
|
+
* @param {string|URL} path A path, a `file://` URL string, or a URL.
|
|
155
|
+
* @param {{ priority?: number, watch?: boolean }} [options]
|
|
156
|
+
* @returns {StyleSheet}
|
|
157
|
+
*/
|
|
158
|
+
addFile(path, options = {}) {
|
|
159
|
+
const file = toPath(path)
|
|
160
|
+
const existing = this.cssEntries.get(file)
|
|
161
|
+
if (existing && existing.size) {
|
|
162
|
+
// Refresh the existing sheet rather than stacking a second provider.
|
|
163
|
+
const entry = existing.values().next().value
|
|
164
|
+
if (entry.provider) entry.provider.loadFromPath(file)
|
|
165
|
+
return this.handle(entry)
|
|
166
|
+
}
|
|
167
|
+
const watch = options.watch ?? HOT_RELOAD
|
|
168
|
+
const entry = { kind: 'file', path: file, priority: options.priority, provider: null, cancelled: false, watch }
|
|
169
|
+
this.trackCssEntry(entry) // register by path now, so re-adds dedup even in production
|
|
170
|
+
this.place(entry)
|
|
171
|
+
if (watch) this.watch(file)
|
|
172
|
+
return this.handle(entry)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Install everything queued before the display existed, and start watching.
|
|
177
|
+
* Call once from your `activate` handler. Safe to call more than once.
|
|
178
|
+
*/
|
|
179
|
+
install() {
|
|
180
|
+
if (this.ready) return
|
|
181
|
+
// Before the display exists, providers would never be shown — fail loudly
|
|
182
|
+
// rather than silently drop them. (place()'s auto-flush only runs once a
|
|
183
|
+
// display exists, so it never trips this.)
|
|
184
|
+
if (!defaultDisplay())
|
|
185
|
+
throw new Error('styles.install() called before the display exists — call it from your app\'s "activate" handler')
|
|
186
|
+
this.ready = true
|
|
187
|
+
const pending = this.queued
|
|
188
|
+
this.queued = []
|
|
189
|
+
for (const entry of pending) this.installEntry(entry)
|
|
190
|
+
if (HOT_RELOAD) this.startWatcher()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// -------------------------------------------------------------------------
|
|
194
|
+
// installation
|
|
195
|
+
// -------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
place(entry) {
|
|
198
|
+
if (!this.ready && defaultDisplay()) this.install() // auto-flush on first post-display call
|
|
199
|
+
if (this.ready) this.installEntry(entry)
|
|
200
|
+
else this.queued.push(entry)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
installEntry(entry) {
|
|
204
|
+
if (entry.cancelled) return
|
|
205
|
+
if (entry.kind === 'file') {
|
|
206
|
+
const provider = this.newProvider()
|
|
207
|
+
provider.loadFromPath(entry.path)
|
|
208
|
+
this.addProvider(provider, entry.priority)
|
|
209
|
+
entry.provider = provider
|
|
210
|
+
} else {
|
|
211
|
+
const provider = this.newProvider()
|
|
212
|
+
loadCss(provider, entry.render ? entry.render() : entry.css)
|
|
213
|
+
this.addProvider(provider, entry.priority)
|
|
214
|
+
entry.provider = provider
|
|
215
|
+
if (entry.file) this.trackFileProvider(entry.file, provider)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
newProvider() {
|
|
220
|
+
const provider = new (gtk().CssProvider)()
|
|
221
|
+
// Surface parse errors with a friendly line instead of a bare GTK critical.
|
|
222
|
+
try {
|
|
223
|
+
provider.on('parsing-error', (section, error) => {
|
|
224
|
+
let message = 'CSS parse error'
|
|
225
|
+
try { if (error && error.message) message += `: ${error.message}` } catch {}
|
|
226
|
+
console.warn(`[node-gtk:styles] ${message}`)
|
|
227
|
+
})
|
|
228
|
+
} catch {}
|
|
229
|
+
return provider
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
addProvider(provider, priority) {
|
|
233
|
+
const display = defaultDisplay()
|
|
234
|
+
if (display) gtk().StyleContext.addProviderForDisplay(display, provider, priority ?? defaultPriority())
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
removeProvider(provider) {
|
|
238
|
+
const display = defaultDisplay()
|
|
239
|
+
if (display) gtk().StyleContext.removeProviderForDisplay(display, provider)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// -------------------------------------------------------------------------
|
|
243
|
+
// handles
|
|
244
|
+
// -------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
handle(entry) {
|
|
247
|
+
return {
|
|
248
|
+
update: (next) => {
|
|
249
|
+
if (entry.kind === 'file') {
|
|
250
|
+
const nextPath = toPath(next)
|
|
251
|
+
if (nextPath !== entry.path) {
|
|
252
|
+
// Re-key tracking + watching to the new path.
|
|
253
|
+
this.untrackCssEntry(entry)
|
|
254
|
+
entry.path = nextPath
|
|
255
|
+
this.trackCssEntry(entry)
|
|
256
|
+
if (entry.watch) this.watch(nextPath)
|
|
257
|
+
}
|
|
258
|
+
if (entry.provider) entry.provider.loadFromPath(entry.path)
|
|
259
|
+
} else {
|
|
260
|
+
// A literal replaces a render function: from now on this is fixed CSS.
|
|
261
|
+
entry.render = null
|
|
262
|
+
entry.css = next
|
|
263
|
+
if (entry.provider) loadCss(entry.provider, next)
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
// Re-apply the sheet from its current source: re-run the render function
|
|
267
|
+
// (inline) or re-read the path (file). For a fixed-string inline sheet this
|
|
268
|
+
// just re-loads the same CSS. Use it when the state a render reads changes.
|
|
269
|
+
refresh: () => {
|
|
270
|
+
if (!entry.provider) return
|
|
271
|
+
if (entry.kind === 'file') entry.provider.loadFromPath(entry.path)
|
|
272
|
+
else loadCss(entry.provider, entry.render ? entry.render() : entry.css)
|
|
273
|
+
},
|
|
274
|
+
remove: () => {
|
|
275
|
+
if (entry.cancelled) return
|
|
276
|
+
entry.cancelled = true
|
|
277
|
+
if (entry.provider) this.removeProvider(entry.provider)
|
|
278
|
+
if (entry.kind === 'file') this.untrackCssEntry(entry)
|
|
279
|
+
else this.untrackFileProvider(entry)
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// -------------------------------------------------------------------------
|
|
285
|
+
// hot-reload (dev only)
|
|
286
|
+
// -------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
trackFileProvider(file, provider) {
|
|
289
|
+
let set = this.fileProviders.get(file)
|
|
290
|
+
if (!set) this.fileProviders.set(file, (set = new Set()))
|
|
291
|
+
set.add(provider)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
trackCssEntry(entry) {
|
|
295
|
+
let set = this.cssEntries.get(entry.path)
|
|
296
|
+
if (!set) this.cssEntries.set(entry.path, (set = new Set()))
|
|
297
|
+
set.add(entry)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// When the last sheet for a path is removed, stop watching it — otherwise a
|
|
301
|
+
// later edit would be misrouted through reloadModule and import() the .css.
|
|
302
|
+
untrackCssEntry(entry) {
|
|
303
|
+
const set = this.cssEntries.get(entry.path)
|
|
304
|
+
if (!set) return
|
|
305
|
+
set.delete(entry)
|
|
306
|
+
if (set.size === 0) {
|
|
307
|
+
this.cssEntries.delete(entry.path)
|
|
308
|
+
this.unwatch(entry.path)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Drop the provider so the next module reload doesn't re-remove it. Keep
|
|
313
|
+
// watching: re-running the module re-installs its add() calls.
|
|
314
|
+
untrackFileProvider(entry) {
|
|
315
|
+
if (!entry.file || !entry.provider) return
|
|
316
|
+
this.fileProviders.get(entry.file)?.delete(entry.provider)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
watch(file) {
|
|
320
|
+
if (this.watchedFiles.has(file)) return
|
|
321
|
+
this.watchedFiles.add(file)
|
|
322
|
+
if (this.ready) this.watchDir(Path.dirname(file)) // else startWatcher picks it up
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Stop watching a file; cancel the directory monitor once its last file goes.
|
|
326
|
+
unwatch(file) {
|
|
327
|
+
if (!this.watchedFiles.delete(file)) return
|
|
328
|
+
const timer = this.reloadTimers.get(file)
|
|
329
|
+
if (timer) { glib().sourceRemove(timer); this.reloadTimers.delete(file) }
|
|
330
|
+
const dir = Path.dirname(file)
|
|
331
|
+
for (const f of this.watchedFiles) if (Path.dirname(f) === dir) return
|
|
332
|
+
const monitor = this.dirWatchers.get(dir)
|
|
333
|
+
if (monitor) { try { monitor.cancel() } catch {} ; this.dirWatchers.delete(dir) }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
startWatcher() {
|
|
337
|
+
for (const file of this.watchedFiles) this.watchDir(Path.dirname(file))
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Watch with a GLib GFileMonitor, not node's fs.watch: it is driven by the
|
|
341
|
+
// GLib loop the app already runs (an fs.watch, a libuv handle, silently stops
|
|
342
|
+
// firing once that loop is the only one running) and it does not keep Node
|
|
343
|
+
// alive on exit. We watch the directory, not the file, so atomic-save renames
|
|
344
|
+
// survive — WATCH_MOVES surfaces them.
|
|
345
|
+
watchDir(dir) {
|
|
346
|
+
if (this.dirWatchers.has(dir)) return
|
|
347
|
+
let monitor
|
|
348
|
+
try {
|
|
349
|
+
const Gio = gio()
|
|
350
|
+
const gfile = Gio.File.newForPath(dir)
|
|
351
|
+
// GFile is a GInterface: its methods live on the prototype, not the instance.
|
|
352
|
+
monitor = Gio.File.prototype.monitorDirectory.call(gfile, Gio.FileMonitorFlags.WATCH_MOVES, null)
|
|
353
|
+
} catch {
|
|
354
|
+
return // Gio unavailable / monitor failed — hot-reload just won't fire
|
|
355
|
+
}
|
|
356
|
+
monitor.on('changed', (file, otherFile) => {
|
|
357
|
+
// Map the event back to a watched file (a rename's target is in otherFile);
|
|
358
|
+
// if neither matches, re-check the dir's files — the debounce coalesces.
|
|
359
|
+
const a = gioPath(file)
|
|
360
|
+
const b = gioPath(otherFile)
|
|
361
|
+
if (a && this.watchedFiles.has(a)) this.onFileChanged(a)
|
|
362
|
+
else if (b && this.watchedFiles.has(b)) this.onFileChanged(b)
|
|
363
|
+
else for (const f of this.watchedFiles) if (Path.dirname(f) === dir) this.onFileChanged(f)
|
|
364
|
+
})
|
|
365
|
+
this.dirWatchers.set(dir, monitor)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// GLib timeout, not setTimeout, so the debounce is loop-driven too.
|
|
369
|
+
onFileChanged(file) {
|
|
370
|
+
const pending = this.reloadTimers.get(file)
|
|
371
|
+
if (pending) glib().sourceRemove(pending)
|
|
372
|
+
this.reloadTimers.set(file, glib().timeoutAdd(0, DEBOUNCE_MS, () => {
|
|
373
|
+
this.reloadTimers.delete(file)
|
|
374
|
+
void this.reloadFile(file)
|
|
375
|
+
return false // GLib.SOURCE_REMOVE
|
|
376
|
+
}))
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async reloadFile(file) {
|
|
380
|
+
// Dispatch by which registry the file is in; one in neither (e.g. its sheets
|
|
381
|
+
// were removed) is ignored, so a stray .css edit is never import()ed.
|
|
382
|
+
const cssEntries = this.cssEntries.get(file)
|
|
383
|
+
if (cssEntries && cssEntries.size) {
|
|
384
|
+
for (const entry of cssEntries) {
|
|
385
|
+
if (entry.provider) {
|
|
386
|
+
try { entry.provider.loadFromPath(file) } catch {}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
console.info(`[node-gtk:styles] reloaded ${Path.relative(process.cwd(), file)}`)
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
if (this.fileProviders.has(file)) await this.reloadModule(file)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Re-import a source module (cache-busted) so its add() calls reinstall the
|
|
397
|
+
* new CSS, then drop the previous run's providers — new sheets up before old
|
|
398
|
+
* come down. A load error rolls back to the previously working sheets.
|
|
399
|
+
*/
|
|
400
|
+
async reloadModule(file) {
|
|
401
|
+
if (this.reloading.has(file)) { this.reloadPending.add(file); return }
|
|
402
|
+
this.reloading.add(file)
|
|
403
|
+
|
|
404
|
+
const previous = this.fileProviders.get(file) ?? new Set()
|
|
405
|
+
const fresh = new Set()
|
|
406
|
+
this.fileProviders.set(file, fresh) // the re-run's tracking collects into here
|
|
407
|
+
try {
|
|
408
|
+
await import(`${pathToFileURL(file).href}?node-gtk-style=${++this.reloadSeq}`)
|
|
409
|
+
for (const provider of previous) this.removeProvider(provider)
|
|
410
|
+
console.info(`[node-gtk:styles] reloaded ${Path.relative(process.cwd(), file)}`)
|
|
411
|
+
} catch (error) {
|
|
412
|
+
for (const provider of fresh) this.removeProvider(provider)
|
|
413
|
+
this.fileProviders.set(file, previous) // keep the working sheets installed
|
|
414
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
415
|
+
console.warn(`[node-gtk:styles] hot-reload failed for ${Path.relative(process.cwd(), file)}: ${message}`)
|
|
416
|
+
} finally {
|
|
417
|
+
this.reloading.delete(file)
|
|
418
|
+
if (this.reloadPending.delete(file)) void this.reloadModule(file)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const styles = new StyleManager()
|
|
424
|
+
|
|
425
|
+
module.exports = {
|
|
426
|
+
StyleManager,
|
|
427
|
+
styles,
|
|
428
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-gtk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "GNOME Gtk+ bindings for NodeJS",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./lib/index.mjs",
|
|
9
|
+
"require": "./lib/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./register": "./lib/esm/register.mjs",
|
|
12
|
+
"./hooks": "./lib/esm/hooks.mjs",
|
|
13
|
+
"./styles": {
|
|
14
|
+
"types": "./lib/styles.d.ts",
|
|
15
|
+
"default": "./lib/styles.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json",
|
|
18
|
+
"./*": "./*"
|
|
19
|
+
},
|
|
6
20
|
"bin": {
|
|
7
21
|
"node-gtk": "bin/node-gtk.js"
|
|
8
22
|
},
|