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/tools/generate-types.js
CHANGED
|
@@ -589,6 +589,35 @@ function emitMethods(info, nFn, getFn, ctx, ownerName, inherited) {
|
|
|
589
589
|
return lines
|
|
590
590
|
}
|
|
591
591
|
|
|
592
|
+
// Virtual functions, emitted as the `virtual_*` override surface that
|
|
593
|
+
// registerClass() wires into the GObject vtable (lib/register-class.js). A
|
|
594
|
+
// subclass overrides a vfunc by defining `virtual_<camelCaseVfuncName>` — e.g.
|
|
595
|
+
// the `size_allocate` vfunc is overridden as `virtual_sizeAllocate`. We emit ALL
|
|
596
|
+
// vfuncs (even invoker-backed ones whose public method is also emitted) so the
|
|
597
|
+
// override surface is explicit and `super.virtual_<name>(...)` chain-up resolves.
|
|
598
|
+
//
|
|
599
|
+
// The signature is modelled exactly like a method's (signature(): IN params,
|
|
600
|
+
// OUT/INOUT folded into a return tuple) because node-gtk marshals a vfunc
|
|
601
|
+
// implementation's out-params from its JS return value (src/callback.cc). The
|
|
602
|
+
// `virtual_` prefix keeps these names distinct from the public invoker method of
|
|
603
|
+
// the same vfunc, so the two never collide (issue #457).
|
|
604
|
+
// LIMITATION: non-primitive OUT params of a vfunc are passed in as objects to be
|
|
605
|
+
// mutated rather than returned; they are surfaced in the return tuple here to
|
|
606
|
+
// match the public-method convention, which is the common (all-primitive) case.
|
|
607
|
+
function emitVFuncs(info, nFn, getFn, ctx, ownerName) {
|
|
608
|
+
const lines = []
|
|
609
|
+
for (const v of each(info, nFn, getFn)) {
|
|
610
|
+
try {
|
|
611
|
+
const rawName = baseName(v)
|
|
612
|
+
const name = 'virtual_' + camelCase(rawName)
|
|
613
|
+
const sig = signature(v, ctx, {})
|
|
614
|
+
const doc = docBlock(ctx, DocKey.fn(ownerName || '', rawName), ' ', { callable: true, deprecated: isDeprecated(v) })
|
|
615
|
+
lines.push(`${doc} ${name}(${sig.params}): ${sig.ret}`)
|
|
616
|
+
} catch (e) { /* skip unrepresentable vfunc */ }
|
|
617
|
+
}
|
|
618
|
+
return lines
|
|
619
|
+
}
|
|
620
|
+
|
|
592
621
|
function emitProperties(info, nFn, getFn, ctx, inherited, containerName) {
|
|
593
622
|
const lines = []
|
|
594
623
|
const writable = []
|
|
@@ -723,6 +752,7 @@ function emitObject(info, ctx) {
|
|
|
723
752
|
const inherited = collectInheritedMethods(info, ctx)
|
|
724
753
|
const props = emitProperties(info, GI.object_info_get_n_properties, GI.object_info_get_property, ctx, inherited, name)
|
|
725
754
|
const methods = emitMethods(info, GI.object_info_get_n_methods, GI.object_info_get_method, ctx, name, inherited)
|
|
755
|
+
const vfuncs = emitVFuncs(info, GI.object_info_get_n_vfuncs, GI.object_info_get_vfunc, ctx, name)
|
|
726
756
|
// When the class merges interfaces, declare the signal API in the companion
|
|
727
757
|
// interface (unified across the whole hierarchy) instead of the class body.
|
|
728
758
|
const signals = hasIfaces ? [] : renderSignals(collectSignals(info, ctx), ctx)
|
|
@@ -746,7 +776,7 @@ function emitObject(info, ctx) {
|
|
|
746
776
|
}
|
|
747
777
|
|
|
748
778
|
const dedup = makeMemberDedup()
|
|
749
|
-
const body = [...constants, ...props.lines, ...methods, ...signals, ...signalApi].filter(dedup)
|
|
779
|
+
const body = [...constants, ...props.lines, ...methods, ...vfuncs, ...signals, ...signalApi].filter(dedup)
|
|
750
780
|
|
|
751
781
|
const out = []
|
|
752
782
|
const ext = parentRef ? ` extends ${parentRef}` : ''
|
|
@@ -787,6 +817,13 @@ function emitInterface(info, ctx) {
|
|
|
787
817
|
const inherited = collectInterfaceInheritedMethods(info, ctx)
|
|
788
818
|
const props = emitProperties(info, GI.interface_info_get_n_properties, GI.interface_info_get_property, ctx, inherited, name)
|
|
789
819
|
const methods = emitMethods(info, GI.interface_info_get_n_methods, GI.interface_info_get_method, ctx, name, inherited)
|
|
820
|
+
// NOTE: interface vfuncs are intentionally NOT emitted. A class implementing
|
|
821
|
+
// several interfaces declaration-merges them into its companion interface, and
|
|
822
|
+
// emitting `virtual_*` members on interfaces that collide across that diamond
|
|
823
|
+
// (e.g. GTK3's Atk Component/TableCell/Action accessibility stack) produces
|
|
824
|
+
// TS2320. Overriding an interface vfunc is rare; object (class) vfuncs — which
|
|
825
|
+
// cover the issue-#457 cases (Widget, GObject lifecycle, LayoutManager) — are
|
|
826
|
+
// emitted in emitObject() and are unaffected.
|
|
790
827
|
|
|
791
828
|
const ext = prereqs.length ? ` extends ${prereqs.join(', ')}` : ''
|
|
792
829
|
const dedup = makeMemberDedup()
|
|
@@ -946,7 +983,6 @@ const SHIM_STATIC_API = ` export function isLoaded(ns: string, version?: string
|
|
|
946
983
|
export function prependLibraryPath(path: string): void
|
|
947
984
|
export function listAvailableModules(): Promise<{ name: string, version: string }[]>
|
|
948
985
|
export function registerClass(klass: Function): Function
|
|
949
|
-
export function startLoop(): void
|
|
950
986
|
export function getGType(value: Function | object | bigint): bigint
|
|
951
987
|
export const System: any`
|
|
952
988
|
|
|
@@ -966,6 +1002,25 @@ function writeShim(outdir, nsVersions) {
|
|
|
966
1002
|
lines.push(` export function require(ns: string, version?: string): any`)
|
|
967
1003
|
lines.push(SHIM_STATIC_API)
|
|
968
1004
|
lines.push(`}`)
|
|
1005
|
+
|
|
1006
|
+
// Ambient declarations for the `gi:` ESM import scheme, so
|
|
1007
|
+
// `import Gtk from 'gi:Gtk-4.0'` is typed as the namespace object (the same
|
|
1008
|
+
// type `gi.require('Gtk','4.0')` returns). The versionless `gi:Gtk` alias
|
|
1009
|
+
// resolves to the version generated here. The `gi:*` fallback does NOT make
|
|
1010
|
+
// un-generated namespaces `any` — its default export is typed as an
|
|
1011
|
+
// instruction string, so any use of one is a TypeScript error that names the
|
|
1012
|
+
// fix (a more specific `gi:<Ns>-<v>` declaration above always wins over it).
|
|
1013
|
+
lines.push(``)
|
|
1014
|
+
for (const [ns, version] of nsVersions) {
|
|
1015
|
+
const type = `typeof import('./${ns}-${version}.js')`
|
|
1016
|
+
lines.push(`declare module 'gi:${ns}-${version}' { const ns: ${type}; export default ns }`)
|
|
1017
|
+
lines.push(`declare module 'gi:${ns}' { const ns: ${type}; export default ns }`)
|
|
1018
|
+
}
|
|
1019
|
+
lines.push(`declare module 'gi:*' {`)
|
|
1020
|
+
lines.push(` const ns: 'node-gtk: no generated types for this gi: namespace. Run: node-gtk generate-types <Namespace>-<version>'`)
|
|
1021
|
+
lines.push(` export default ns`)
|
|
1022
|
+
lines.push(`}`)
|
|
1023
|
+
|
|
969
1024
|
fs.writeFileSync(path.join(outdir, 'node-gtk.d.ts'), lines.join('\n') + '\n')
|
|
970
1025
|
}
|
|
971
1026
|
|
|
@@ -1007,6 +1062,16 @@ const DEFAULT_OUTDIR = ['node_modules', '.node-gtk-types']
|
|
|
1007
1062
|
// Doc comments are pulled from .gir XML; toggled off with --no-docs.
|
|
1008
1063
|
let DOCS_ENABLED = true
|
|
1009
1064
|
|
|
1065
|
+
const INSTALL_URL = 'https://github.com/romgrk/node-gtk#installing'
|
|
1066
|
+
|
|
1067
|
+
// A missing typelib means the native GI libraries (gtk4, libadwaita, …) aren't
|
|
1068
|
+
// installed on this machine. g_irepository_require surfaces that as a "Typelib
|
|
1069
|
+
// file for namespace … not found" error — recognize it so we can replace the
|
|
1070
|
+
// opaque stack trace with actionable install guidance.
|
|
1071
|
+
function isMissingTypelibError(err) {
|
|
1072
|
+
return /Typelib file for namespace/i.test((err && err.message) || '')
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1010
1075
|
const USAGE = `Usage: node-gtk generate-types <Namespace-Version> [...] [options]
|
|
1011
1076
|
|
|
1012
1077
|
Generates TypeScript declarations for the given GObject-Introspection
|
|
@@ -1036,7 +1101,19 @@ function run(argv) {
|
|
|
1036
1101
|
roots.push(argv[i])
|
|
1037
1102
|
}
|
|
1038
1103
|
if (roots.length === 0) { console.error(USAGE); process.exit(1) }
|
|
1039
|
-
|
|
1104
|
+
try {
|
|
1105
|
+
generate(roots, outdir)
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
if (isMissingTypelibError(err)) {
|
|
1108
|
+
console.error(
|
|
1109
|
+
`\nnode-gtk generate-types: ${err.message}\n\n` +
|
|
1110
|
+
`The native GTK / GObject-Introspection libraries (e.g. gtk4, libadwaita)\n` +
|
|
1111
|
+
`don't seem to be installed. Install them for your platform, then retry:\n` +
|
|
1112
|
+
` ${INSTALL_URL}\n`)
|
|
1113
|
+
process.exit(1)
|
|
1114
|
+
}
|
|
1115
|
+
throw err
|
|
1116
|
+
}
|
|
1040
1117
|
}
|
|
1041
1118
|
|
|
1042
1119
|
module.exports = { generate, run }
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* list-libraries.js — list the GObject-Introspection libraries (typelibs)
|
|
3
|
+
* available on this machine, grouped by namespace with their versions.
|
|
4
|
+
*
|
|
5
|
+
* Driven by the CLI: `node-gtk list [filter] [options]`. The names/versions it
|
|
6
|
+
* prints are exactly what you pass to `require()` / the `gi:` import scheme /
|
|
7
|
+
* `generate-types` (e.g. `Gtk-4.0`).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const gi = require('../lib/index.js')
|
|
11
|
+
const GI = gi._GIRepository
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// data
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
// Compare dotted versions numerically, ascending ("3.0" < "4.0" < "10.0"),
|
|
18
|
+
// falling back to a string compare for non-numeric segments.
|
|
19
|
+
function compareVersions(a, b) {
|
|
20
|
+
const pa = a.split('.'), pb = b.split('.')
|
|
21
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
22
|
+
const x = pa[i] ?? '', y = pb[i] ?? ''
|
|
23
|
+
const nx = Number(x), ny = Number(y)
|
|
24
|
+
if (Number.isNaN(nx) || Number.isNaN(ny)) { if (x !== y) return x < y ? -1 : 1 }
|
|
25
|
+
else if (nx !== ny) return nx - ny
|
|
26
|
+
}
|
|
27
|
+
return 0
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Group the flat [{name, version}] from listAvailableModules() into a sorted
|
|
31
|
+
// list of { name, versions } (names A→Z, versions ascending, deduped).
|
|
32
|
+
async function collect(filter) {
|
|
33
|
+
const modules = await gi.listAvailableModules()
|
|
34
|
+
const byName = new Map()
|
|
35
|
+
for (const { name, version } of modules) {
|
|
36
|
+
if (filter && !name.toLowerCase().includes(filter.toLowerCase())) continue
|
|
37
|
+
if (!byName.has(name)) byName.set(name, new Set())
|
|
38
|
+
if (version) byName.get(name).add(version)
|
|
39
|
+
}
|
|
40
|
+
return [...byName.entries()]
|
|
41
|
+
.map(([name, versions]) => ({ name, versions: [...versions].sort(compareVersions) }))
|
|
42
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// output
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
const useColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR
|
|
50
|
+
const ansi = (open, close) => (s) => useColor ? `\x1b[${open}m${s}\x1b[${close}m` : s
|
|
51
|
+
const bold = ansi(1, 22)
|
|
52
|
+
const dim = ansi(2, 22)
|
|
53
|
+
const cyan = ansi(36, 39)
|
|
54
|
+
|
|
55
|
+
function printTable(libs) {
|
|
56
|
+
if (libs.length === 0) {
|
|
57
|
+
process.stdout.write(`${dim('No libraries found.')}\n`)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
const width = libs.reduce((w, l) => Math.max(w, l.name.length), 0)
|
|
61
|
+
for (const { name, versions } of libs)
|
|
62
|
+
process.stdout.write(` ${bold(name.padEnd(width))} ${dim(versions.join(', '))}\n`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// cli — `node-gtk list [filter] [options]`
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
const HELP = `node-gtk list — list the GObject-Introspection libraries available on this machine
|
|
70
|
+
|
|
71
|
+
Usage:
|
|
72
|
+
node-gtk list [filter] [options]
|
|
73
|
+
|
|
74
|
+
Arguments:
|
|
75
|
+
filter Only show libraries whose name contains this (case-insensitive)
|
|
76
|
+
|
|
77
|
+
Options:
|
|
78
|
+
--json Output as JSON: [{ "name", "versions": [...] }]
|
|
79
|
+
-h, --help Show this help
|
|
80
|
+
|
|
81
|
+
Examples:
|
|
82
|
+
node-gtk list
|
|
83
|
+
node-gtk list gtk
|
|
84
|
+
node-gtk list --json
|
|
85
|
+
`
|
|
86
|
+
|
|
87
|
+
function run(argv) {
|
|
88
|
+
// Don't crash when the output is piped into a reader that closes early
|
|
89
|
+
// (e.g. `node-gtk list | head`).
|
|
90
|
+
process.stdout.on('error', (err) => { if (err.code === 'EPIPE') process.exit(0); throw err })
|
|
91
|
+
|
|
92
|
+
const opts = { json: false }
|
|
93
|
+
for (const arg of argv) {
|
|
94
|
+
if (arg === '-h' || arg === '--help') { process.stdout.write(HELP); return }
|
|
95
|
+
else if (arg === '--json') opts.json = true
|
|
96
|
+
else if (arg.startsWith('-')) { process.stderr.write(`node-gtk list: unknown option '${arg}'\n\n${HELP}`); process.exit(1) }
|
|
97
|
+
else if (opts.filter === undefined) opts.filter = arg
|
|
98
|
+
else { process.stderr.write(`node-gtk list: unexpected argument '${arg}'\n\n${HELP}`); process.exit(1) }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
collect(opts.filter).then((libs) => {
|
|
102
|
+
if (opts.json) {
|
|
103
|
+
process.stdout.write(JSON.stringify(libs, null, 2) + '\n')
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const what = opts.filter ? `matching ${JSON.stringify(opts.filter)}` : 'available'
|
|
108
|
+
process.stdout.write(`\n${bold(`${libs.length}`)} ${libs.length === 1 ? 'library' : 'libraries'} ${what}:\n\n`)
|
|
109
|
+
printTable(libs)
|
|
110
|
+
|
|
111
|
+
if (libs.length) {
|
|
112
|
+
const first = libs.find(l => l.versions.length) || libs[0]
|
|
113
|
+
const example = first.versions.length ? `${first.name}-${first.versions[first.versions.length - 1]}` : first.name
|
|
114
|
+
process.stdout.write(`\n${dim('Use one with, e.g.:')} ${cyan(`node-gtk generate-types ${example}`)}\n\n`)
|
|
115
|
+
} else {
|
|
116
|
+
const paths = (GI.Repository_get_search_path() || [])
|
|
117
|
+
process.stdout.write(`\n${dim('Searched: ' + paths.join(', '))}\n`)
|
|
118
|
+
}
|
|
119
|
+
}).catch((err) => {
|
|
120
|
+
process.stderr.write(`node-gtk list: ${err.message}\n`)
|
|
121
|
+
process.exit(1)
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { run, collect, compareVersions }
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# __APP_NAME__
|
|
2
|
+
|
|
3
|
+
An [Adwaita](https://gnome.pages.gitlab.gnome.org/libadwaita/) application built
|
|
4
|
+
with [node-gtk](https://github.com/romgrk/node-gtk) and TypeScript.
|
|
5
|
+
|
|
6
|
+
## Prerequisites
|
|
7
|
+
|
|
8
|
+
You need GTK 4 and libadwaita — plus their GObject-Introspection data — installed
|
|
9
|
+
on your system. node-gtk talks to the libraries you actually have installed.
|
|
10
|
+
|
|
11
|
+
- **Fedora:** `sudo dnf install gtk4-devel libadwaita-devel gobject-introspection-devel`
|
|
12
|
+
- **Debian/Ubuntu:** `sudo apt install libgtk-4-dev libadwaita-1-dev gobject-introspection`
|
|
13
|
+
- **Arch:** `sudo pacman -S gtk4 libadwaita gobject-introspection`
|
|
14
|
+
- **macOS (Homebrew):** `brew install gtk4 libadwaita gobject-introspection`
|
|
15
|
+
|
|
16
|
+
The `-dev`/`-devel` packages also ship the `.gir` files used to embed GNOME's API
|
|
17
|
+
documentation into the generated TypeScript types (shown on hover in your editor).
|
|
18
|
+
|
|
19
|
+
You also need **Node.js ≥ 20.6** (for the `gi:` import hooks).
|
|
20
|
+
|
|
21
|
+
## Getting started
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install # installs deps and generates TypeScript types (postinstall)
|
|
25
|
+
npm run dev # run the app with live reload
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That's it — a window should appear. Edit `style.css` while it runs and the
|
|
29
|
+
window restyles instantly, no restart. To also restart the app when you edit
|
|
30
|
+
`src/`, run `npm run dev:app-reload` instead (it adds `node --watch`).
|
|
31
|
+
|
|
32
|
+
## How it works
|
|
33
|
+
|
|
34
|
+
Namespaces are imported with the `gi:` scheme, and their default export is the
|
|
35
|
+
namespace object:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import Gtk from 'gi:Gtk-4.0'
|
|
39
|
+
import Adw from 'gi:Adw-1'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
For those imports to resolve, node-gtk's loader hooks must be installed, which is
|
|
43
|
+
why the app is run with `node --import node-gtk/register`. The `dev`/`start`
|
|
44
|
+
scripts already include this (along with `tsx`, which runs the TypeScript without
|
|
45
|
+
a separate build step). node-gtk integrates the GTK main loop with Node's event
|
|
46
|
+
loop automatically.
|
|
47
|
+
|
|
48
|
+
## Scripts
|
|
49
|
+
|
|
50
|
+
| command | what it does |
|
|
51
|
+
| ------------------------ | ----------------------------------------------------------------- |
|
|
52
|
+
| `npm run dev` | Run with live CSS reload — edit `style.css`, the window restyles, no restart. |
|
|
53
|
+
| `npm run dev:app-reload` | Like `dev`, but also restarts the app when you edit `src/` (`node --watch`). |
|
|
54
|
+
| `npm start` | Run once without building. |
|
|
55
|
+
| `npm run build` | Type-check and compile `src/` to `dist/` with `tsc`. |
|
|
56
|
+
| `npm run typecheck` | Type-check only, no output. |
|
|
57
|
+
| `npm run generate-types` | (Re)generate TypeScript types for the GI namespaces you use. |
|
|
58
|
+
|
|
59
|
+
## TypeScript types
|
|
60
|
+
|
|
61
|
+
Types are generated **on your machine** from the GObject-Introspection typelibs
|
|
62
|
+
you have installed, so they match your actual library versions and node-gtk's
|
|
63
|
+
runtime shape (camelCase methods, typed signals, nullability, …). They live in
|
|
64
|
+
`node_modules/.node-gtk-types/` (git-ignored) and `tsconfig.json` points at them.
|
|
65
|
+
|
|
66
|
+
The `postinstall` script regenerates them automatically. If you start using
|
|
67
|
+
another library, add it to the `generate-types` script in `package.json` and
|
|
68
|
+
re-run it:
|
|
69
|
+
|
|
70
|
+
```jsonc
|
|
71
|
+
// package.json
|
|
72
|
+
"generate-types": "node-gtk generate-types Gtk-4.0 Adw-1 GtkSource-5"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
npm run generate-types
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Project structure
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
.
|
|
83
|
+
├── src/
|
|
84
|
+
│ ├── main.ts # the application entry point
|
|
85
|
+
│ └── welcome.ts # a component with its own inline, hot-reloadable styles
|
|
86
|
+
├── style.css # custom CSS (hot-reloads live under `npm run dev`)
|
|
87
|
+
├── tsconfig.json
|
|
88
|
+
└── package.json
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Learn more
|
|
92
|
+
|
|
93
|
+
- [node-gtk](https://github.com/romgrk/node-gtk) — the bindings
|
|
94
|
+
- [Importing libraries](https://github.com/romgrk/node-gtk/blob/master/doc/importing.md) — the `gi:` scheme and ESM details
|
|
95
|
+
- [GTK 4 API reference](https://docs.gtk.org/gtk4/)
|
|
96
|
+
- [libadwaita API reference](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/)
|
|
97
|
+
- [Adwaita style classes](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/style-classes.html)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PKG_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "An Adwaita application built with node-gtk.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": true,
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "npm run dev:css-reload",
|
|
9
|
+
"dev:css-reload": "cross-env NODE_ENV=development node --import tsx --import node-gtk/register src/main.ts",
|
|
10
|
+
"dev:app-reload": "cross-env NODE_ENV=development node --import tsx --import node-gtk/register --watch src/main.ts",
|
|
11
|
+
"start": "node --import tsx --import node-gtk/register src/main.ts",
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"typecheck": "tsc --noEmit",
|
|
14
|
+
"generate-types": "node-gtk generate-types Gtk-4.0 Adw-1",
|
|
15
|
+
"postinstall": "npm run generate-types"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"node-gtk": "__NODE_GTK_VERSION__"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.0.0",
|
|
22
|
+
"cross-env": "^7.0.3",
|
|
23
|
+
"tsx": "^4.19.0",
|
|
24
|
+
"typescript": "^5.5.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* __APP_NAME__ — an Adwaita application built with node-gtk.
|
|
3
|
+
*
|
|
4
|
+
* Namespaces are imported with the `gi:` scheme (`import Gtk from 'gi:Gtk-4.0'`);
|
|
5
|
+
* the app is run with `node --import node-gtk/register …` (see the package.json
|
|
6
|
+
* scripts). node-gtk integrates the GTK main loop with Node's event loop
|
|
7
|
+
* automatically — there's nothing to call to enable it.
|
|
8
|
+
*
|
|
9
|
+
* One ESM caveat: the blocking run call (`loop.run()` / `app.run()`) RETURNS
|
|
10
|
+
* IMMEDIATELY instead of blocking. So `app.run()` is the last statement and we
|
|
11
|
+
* tear everything down from the window's close handler / quit action.
|
|
12
|
+
* See: https://github.com/romgrk/node-gtk/blob/master/doc/importing.md
|
|
13
|
+
*/
|
|
14
|
+
import GLib from 'gi:GLib-2.0'
|
|
15
|
+
import Gio from 'gi:Gio-2.0'
|
|
16
|
+
import Gtk from 'gi:Gtk-4.0'
|
|
17
|
+
import Adw from 'gi:Adw-1'
|
|
18
|
+
|
|
19
|
+
import { styles } from 'node-gtk/styles'
|
|
20
|
+
|
|
21
|
+
// A component in its own module (note the `.js` specifier — TypeScript's NodeNext
|
|
22
|
+
// resolution wants the compiled extension even though the file is welcome.ts).
|
|
23
|
+
import { createWelcome } from './welcome.js'
|
|
24
|
+
|
|
25
|
+
const APP_ID = '__APP_ID__'
|
|
26
|
+
|
|
27
|
+
const loop = GLib.MainLoop.new(null, false)
|
|
28
|
+
const app = new Adw.Application({ applicationId: APP_ID, flags: Gio.ApplicationFlags.FLAGS_NONE })
|
|
29
|
+
|
|
30
|
+
app.on('activate', () => {
|
|
31
|
+
// Apply style.css, layered on top of Adwaita. Under `npm run dev` the file is
|
|
32
|
+
// re-read live as you edit it — no restart, no flash. style.css sits at the
|
|
33
|
+
// project root, one level up from both src/ (dev, via tsx) and dist/ (built,
|
|
34
|
+
// via node), so this URL resolves in either case.
|
|
35
|
+
styles.addFile(new URL('../style.css', import.meta.url))
|
|
36
|
+
|
|
37
|
+
const window = new Adw.ApplicationWindow({ application: app })
|
|
38
|
+
window.setTitle('__APP_NAME__')
|
|
39
|
+
window.setDefaultSize(640, 520)
|
|
40
|
+
|
|
41
|
+
// ---- header bar with a primary menu ----------------------------------
|
|
42
|
+
const header = new Adw.HeaderBar()
|
|
43
|
+
|
|
44
|
+
const menu = new Gio.Menu()
|
|
45
|
+
menu.append('About __APP_NAME__', 'app.about')
|
|
46
|
+
menu.append('Quit', 'app.quit')
|
|
47
|
+
|
|
48
|
+
const menuButton = new Gtk.MenuButton({
|
|
49
|
+
iconName: 'open-menu-symbolic',
|
|
50
|
+
menuModel: menu,
|
|
51
|
+
primary: true,
|
|
52
|
+
})
|
|
53
|
+
header.packEnd(menuButton)
|
|
54
|
+
|
|
55
|
+
// ---- content: a welcome screen ---------------------------------------
|
|
56
|
+
// Adw.ToastOverlay lets us pop transient "toast" notifications.
|
|
57
|
+
const toasts = new Adw.ToastOverlay({ vexpand: true })
|
|
58
|
+
|
|
59
|
+
// The welcome screen lives in its own module (src/welcome.ts) so its inline
|
|
60
|
+
// styles.add() CSS hot-reloads on its own — see that file.
|
|
61
|
+
toasts.setChild(createWelcome(() => {
|
|
62
|
+
toasts.addToast(new Adw.Toast({ title: 'Hello from __APP_NAME__ 👋' }))
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
// Adw.ApplicationWindow has no built-in title bar, so we stack our own
|
|
66
|
+
// header above the content inside a vertical box.
|
|
67
|
+
const content = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL })
|
|
68
|
+
content.append(header)
|
|
69
|
+
content.append(toasts)
|
|
70
|
+
window.setContent(content)
|
|
71
|
+
|
|
72
|
+
// ---- app actions (wired to the menu above) ---------------------------
|
|
73
|
+
const showAbout = () => {
|
|
74
|
+
// Adw.AboutWindow works on libadwaita 1.2+. On 1.5+ you can switch to
|
|
75
|
+
// Adw.AboutDialog (a Gtk.Window-free dialog) if you prefer.
|
|
76
|
+
const about = new Adw.AboutWindow({
|
|
77
|
+
transientFor: window,
|
|
78
|
+
applicationName: '__APP_NAME__',
|
|
79
|
+
applicationIcon: APP_ID,
|
|
80
|
+
developerName: 'Your Name',
|
|
81
|
+
version: '0.1.0',
|
|
82
|
+
comments: 'An Adwaita application built with node-gtk.',
|
|
83
|
+
developers: ['Your Name <you@example.com>'],
|
|
84
|
+
copyright: '© 2026 Your Name',
|
|
85
|
+
licenseType: Gtk.License.MIT_X11,
|
|
86
|
+
})
|
|
87
|
+
about.present()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const aboutAction = Gio.SimpleAction.new('about', null)
|
|
91
|
+
aboutAction.on('activate', () => showAbout())
|
|
92
|
+
app.addAction(aboutAction)
|
|
93
|
+
|
|
94
|
+
const quitAction = Gio.SimpleAction.new('quit', null)
|
|
95
|
+
quitAction.on('activate', () => { loop.quit(); app.quit() })
|
|
96
|
+
app.addAction(quitAction)
|
|
97
|
+
app.setAccelsForAction('app.quit', ['<Control>q'])
|
|
98
|
+
|
|
99
|
+
window.on('close-request', () => (loop.quit(), app.quit(), false))
|
|
100
|
+
|
|
101
|
+
styles.install() // flush queued styles and start the dev hot-reload watcher
|
|
102
|
+
window.present()
|
|
103
|
+
|
|
104
|
+
// The loop integration is already running; under ESM this returns immediately.
|
|
105
|
+
// The app keeps running until the close handler (or quit action) stops it.
|
|
106
|
+
loop.run()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// Must be the last statement — returns immediately under ESM (see top of file).
|
|
110
|
+
app.run([])
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* welcome.ts — the welcome screen, in its own component module.
|
|
3
|
+
*
|
|
4
|
+
* The inline CSS below is registered with styles.add(). Because this module's
|
|
5
|
+
* top level only registers styles and defines an export — it builds no widgets
|
|
6
|
+
* until createWelcome() is called — node-gtk/styles can safely re-execute it on
|
|
7
|
+
* every edit: tweak the CSS and the running window restyles live, no restart.
|
|
8
|
+
* (Editing the widget code instead needs a restart — use `npm run dev:app-reload`.)
|
|
9
|
+
*/
|
|
10
|
+
import Gtk from 'gi:Gtk-4.0'
|
|
11
|
+
import Adw from 'gi:Adw-1'
|
|
12
|
+
|
|
13
|
+
import { styles } from 'node-gtk/styles'
|
|
14
|
+
|
|
15
|
+
// Inline, hot-reloadable styles for this component. Edit a value and save to
|
|
16
|
+
// see it apply live under `npm run dev`.
|
|
17
|
+
styles.add(`
|
|
18
|
+
.welcome-button {
|
|
19
|
+
font-weight: bold;
|
|
20
|
+
}
|
|
21
|
+
`)
|
|
22
|
+
|
|
23
|
+
// Build the welcome screen. `onSayHello` runs when the button is clicked.
|
|
24
|
+
export function createWelcome(onSayHello: () => void) {
|
|
25
|
+
// Adw.StatusPage is the idiomatic "empty state" / welcome widget.
|
|
26
|
+
const welcome = new Adw.StatusPage({
|
|
27
|
+
iconName: 'face-smile-symbolic',
|
|
28
|
+
title: 'Welcome to __APP_NAME__',
|
|
29
|
+
description: 'Edit src/welcome.ts (its styles hot-reload) or src/main.ts.',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const button = new Gtk.Button({
|
|
33
|
+
label: 'Say hello',
|
|
34
|
+
halign: Gtk.Align.CENTER,
|
|
35
|
+
cssClasses: ['pill', 'suggested-action', 'welcome-button'],
|
|
36
|
+
})
|
|
37
|
+
button.on('clicked', onSayHello)
|
|
38
|
+
welcome.setChild(button)
|
|
39
|
+
|
|
40
|
+
return welcome
|
|
41
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Custom styles for __APP_NAME__.
|
|
3
|
+
*
|
|
4
|
+
* Loaded by src/main.ts (via node-gtk/styles), layered on top of the Adwaita
|
|
5
|
+
* stylesheet. Under `npm run dev` it hot-reloads — edit and save to see changes
|
|
6
|
+
* live, with no restart. See the GTK4 CSS reference:
|
|
7
|
+
* https://docs.gtk.org/gtk4/css-overview.html
|
|
8
|
+
* https://docs.gtk.org/gtk4/css-properties.html
|
|
9
|
+
*
|
|
10
|
+
* Adwaita ships many ready-made style classes (`.pill`, `.suggested-action`,
|
|
11
|
+
* `.title-1`, `.dim-label`, …) — prefer those before writing custom CSS:
|
|
12
|
+
* https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/style-classes.html
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/* Example: nudge the welcome button. Delete or edit freely. */
|
|
16
|
+
button.suggested-action.pill {
|
|
17
|
+
padding-left: 24px;
|
|
18
|
+
padding-right: 24px;
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022"],
|
|
5
|
+
"module": "nodenext",
|
|
6
|
+
"moduleResolution": "nodenext",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"outDir": "dist",
|
|
11
|
+
"rootDir": "src",
|
|
12
|
+
"types": ["node"],
|
|
13
|
+
"paths": {
|
|
14
|
+
"node-gtk": ["./node_modules/.node-gtk-types/node-gtk.d.ts"]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*.ts"],
|
|
18
|
+
"files": ["node_modules/.node-gtk-types/node-gtk.d.ts"]
|
|
19
|
+
}
|
/package/{COPYING → LICENSE}
RENAMED
|
File without changes
|