node-gtk 2.2.0 → 4.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 +45 -161
- package/bin/node-gtk.js +12 -1
- package/binding.gyp +21 -0
- package/lib/binding/node-v127-linux-x64/node_gtk.node +0 -0
- package/lib/bootstrap.js +43 -11
- 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/overrides/Gtk-4.0.js +6 -27
- package/lib/register-class.js +86 -3
- package/lib/styles.d.ts +81 -0
- package/lib/styles.js +428 -0
- package/package.json +15 -2
- package/src/boxed.cc +13 -5
- package/src/closure.cc +19 -6
- package/src/closure.h +8 -4
- package/src/function.cc +59 -5
- package/src/fundamental.cc +451 -0
- package/src/fundamental.h +71 -0
- package/src/gi.cc +21 -1
- package/src/gobject.cc +268 -9
- package/src/gobject.h +5 -0
- package/src/modules/cairo/context.cc +103 -103
- package/src/modules/cairo/font-extents.cc +6 -2
- package/src/modules/cairo/generator.js +1 -1
- package/src/modules/cairo/glyph.cc +6 -2
- package/src/modules/cairo/path.cc +6 -2
- package/src/modules/cairo/rectangle-int.cc +6 -2
- package/src/modules/cairo/rectangle.cc +6 -2
- package/src/modules/cairo/text-cluster.cc +6 -2
- package/src/modules/cairo/text-extents.cc +6 -2
- package/src/modules/system.cc +4 -4
- package/src/util.h +3 -3
- package/src/value.cc +44 -8
- package/src/value.h +2 -2
- 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/register-class.js
CHANGED
|
@@ -11,17 +11,50 @@ const GObject = module_.require('GObject')
|
|
|
11
11
|
|
|
12
12
|
module.exports = registerClass
|
|
13
13
|
|
|
14
|
+
// Make registerClass() optional: the first `new Subclass()` of an unregistered
|
|
15
|
+
// JS subclass lazily registers it through this hook (see GObjectConstructor in
|
|
16
|
+
// src/gobject.cc). registerClass() stays available for callers that need the
|
|
17
|
+
// GType before constructing (e.g. getGType, GtkBuilder templates).
|
|
18
|
+
internal.SetLazyClassRegister(registerClass)
|
|
19
|
+
|
|
20
|
+
// A registered class owns its `__gtype__` (native classes via the prototype
|
|
21
|
+
// template, JS classes via registerClass below); an unregistered subclass only
|
|
22
|
+
// inherits one. Used to make registration idempotent and to find ancestors that
|
|
23
|
+
// still need registering.
|
|
24
|
+
function isRegistered(klass) {
|
|
25
|
+
return Object.prototype.hasOwnProperty.call(klass.prototype, '__gtype__')
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
/**
|
|
15
|
-
* Create a new GObject type
|
|
29
|
+
* Create a new GObject type.
|
|
30
|
+
*
|
|
31
|
+
* To override a virtual function, define a method named `virtual_` + the
|
|
32
|
+
* camelCase vfunc name (e.g. `virtual_sizeAllocate` overrides
|
|
33
|
+
* `size_allocate`, `virtual_getRequestMode` overrides `get_request_mode`).
|
|
34
|
+
* Only `virtual_*`-prefixed methods are wired into the vtable; plain methods are
|
|
35
|
+
* never treated as overrides. Chain up to the parent implementation with
|
|
36
|
+
* `super.virtual_sizeAllocate(...)`. See doc/index.md "Inheritance".
|
|
37
|
+
*
|
|
16
38
|
* @param {Class} klass - The class to register
|
|
17
39
|
* @param {string} [klass.GTypeName] - The name of the GType (klass.name by default)
|
|
40
|
+
* @returns {Class} the same class (so it can be assigned or used as a decorator)
|
|
18
41
|
*/
|
|
19
42
|
function registerClass(klass) {
|
|
43
|
+
// Idempotent: a class that already owns a GType is registered. This also makes
|
|
44
|
+
// the lazy-on-first-construct path a no-op for explicitly-registered classes.
|
|
45
|
+
if (isRegistered(klass))
|
|
46
|
+
return klass
|
|
47
|
+
|
|
20
48
|
const parent = Object.getPrototypeOf(klass.prototype).constructor
|
|
21
49
|
|
|
22
50
|
if (!(klass.prototype instanceof GObject.Object))
|
|
23
51
|
throw new Error(`Invalid base class (${parent.name})`)
|
|
24
52
|
|
|
53
|
+
// Register any unregistered ancestor first, so a subclass can be constructed
|
|
54
|
+
// (or registered) without its superclass having been registered by hand.
|
|
55
|
+
if (!isRegistered(parent))
|
|
56
|
+
registerClass(parent)
|
|
57
|
+
|
|
25
58
|
const name = createGTypeName(klass)
|
|
26
59
|
const gtype = GObject.typeFromName(name)
|
|
27
60
|
const parentName = getGTypeName(parent)
|
|
@@ -41,26 +74,51 @@ function registerClass(klass) {
|
|
|
41
74
|
|
|
42
75
|
// Setup virtual functions
|
|
43
76
|
setupVirtualFunctions(klass, klassGtype, parentGtype)
|
|
77
|
+
|
|
78
|
+
return klass
|
|
44
79
|
}
|
|
45
80
|
|
|
46
81
|
// Helpers
|
|
47
82
|
|
|
83
|
+
/* Methods whose name starts with `virtual_` are treated as virtual-function
|
|
84
|
+
* overrides — and *only* those. This makes overriding explicit and opt-in: a
|
|
85
|
+
* plain method named `dispose`, `getProperty`, `sizeAllocate`, … can no longer
|
|
86
|
+
* silently hijack the matching GObject vfunc (issue #457). The prefix also keeps
|
|
87
|
+
* the override name distinct from the public invoker method of the same vfunc
|
|
88
|
+
* (e.g. `widget.sizeAllocate(...)` the method vs. the `virtual_sizeAllocate`
|
|
89
|
+
* override), so the two no longer collide. */
|
|
90
|
+
const VIRTUAL_PREFIX = /^virtual_/
|
|
91
|
+
|
|
92
|
+
/* `virtual_getRequestMode` -> `get_request_mode` (drop the prefix, snake_case the
|
|
93
|
+
* rest). snakeCase('virtual_getRequestMode') === 'virtual_get_request_mode'. */
|
|
94
|
+
function vfuncNativeName(key) {
|
|
95
|
+
return snakeCase(key).replace(/^virtual_/, '')
|
|
96
|
+
}
|
|
97
|
+
|
|
48
98
|
function setupVirtualFunctions(klass, klassGtype, parentGtype) {
|
|
49
99
|
const parentInfo = findInfoByGtype(parentGtype)
|
|
50
100
|
if (!parentInfo)
|
|
51
101
|
throw new Error(`Could not find GIR data in inheritance chain`)
|
|
52
102
|
|
|
103
|
+
const parentPrototype = Object.getPrototypeOf(klass.prototype)
|
|
104
|
+
|
|
53
105
|
Object.getOwnPropertyNames(klass.prototype).forEach(key => {
|
|
54
106
|
if (key === 'constructor')
|
|
55
107
|
return
|
|
108
|
+
if (!VIRTUAL_PREFIX.test(key))
|
|
109
|
+
return
|
|
56
110
|
if (typeof klass.prototype[key] !== 'function')
|
|
57
111
|
return
|
|
58
112
|
|
|
59
|
-
const nativeName =
|
|
113
|
+
const nativeName = vfuncNativeName(key)
|
|
60
114
|
const vfuncInfo = findVFunc(klassGtype, parentInfo, nativeName)
|
|
61
115
|
|
|
62
116
|
if (!vfuncInfo)
|
|
63
|
-
|
|
117
|
+
throw new Error(
|
|
118
|
+
`${klass.name}.${key}: no virtual function '${nativeName}' on ` +
|
|
119
|
+
`'${GObject.typeName(parentGtype)}' or its interfaces. A 'virtual_*' ` +
|
|
120
|
+
`method must name an existing vfunc (e.g. 'virtual_sizeAllocate' for ` +
|
|
121
|
+
`'size_allocate'); rename it if it is a plain method.`)
|
|
64
122
|
|
|
65
123
|
internal.RegisterVFunc(
|
|
66
124
|
vfuncInfo,
|
|
@@ -68,6 +126,31 @@ function setupVirtualFunctions(klass, klassGtype, parentGtype) {
|
|
|
68
126
|
nativeName,
|
|
69
127
|
klass.prototype[key]
|
|
70
128
|
)
|
|
129
|
+
|
|
130
|
+
installParentVFunc(parentPrototype, parentGtype, key, vfuncInfo)
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Make `super.<vfunc>(...)` reachable from an override. The override replaces
|
|
135
|
+
* the parent's implementation in the class vtable, so a JS subclass otherwise
|
|
136
|
+
* has no way to call the implementation it overrode. We install, on the parent
|
|
137
|
+
* GI class's prototype, a method that invokes the *parent's* native vfunc impl
|
|
138
|
+
* (resolved through `parentGtype`'s vtable, not the overriding subclass's).
|
|
139
|
+
*
|
|
140
|
+
* Only the native boundary needs bridging: if the parent prototype already owns
|
|
141
|
+
* `key` — i.e. the parent is itself a registered JS class that overrode this
|
|
142
|
+
* vfunc — then `super.<vfunc>()` resolves to that JS method on its own. */
|
|
143
|
+
function installParentVFunc(parentPrototype, parentGtype, key, vfuncInfo) {
|
|
144
|
+
if (Object.prototype.hasOwnProperty.call(parentPrototype, key))
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
Object.defineProperty(parentPrototype, key, {
|
|
148
|
+
value: function (...args) {
|
|
149
|
+
return internal.CallVFunc(vfuncInfo, parentGtype, this, args)
|
|
150
|
+
},
|
|
151
|
+
writable: true,
|
|
152
|
+
configurable: true,
|
|
153
|
+
enumerable: false,
|
|
71
154
|
})
|
|
72
155
|
}
|
|
73
156
|
|
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": "4.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
|
},
|
|
@@ -50,7 +64,6 @@
|
|
|
50
64
|
"assert": "^1.5.0",
|
|
51
65
|
"aws-sdk": "^2.452.0",
|
|
52
66
|
"chalk": "^2.4.2",
|
|
53
|
-
"deasync": "^0.1.30",
|
|
54
67
|
"mocha": "^7.1.0",
|
|
55
68
|
"nid-parser": "0.0.5",
|
|
56
69
|
"node-pre-gyp-github": "^1.4.5"
|