imxc 0.5.4 → 0.6.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.
@@ -0,0 +1,945 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { buildImxDts, TSCONFIG, GITIGNORE, cmakeTemplate } from './index.js';
4
+ // ---------------------------------------------------------------------------
5
+ // Shared helper content (async.h used by both async and networking)
6
+ // ---------------------------------------------------------------------------
7
+ const ASYNC_H = `#pragma once
8
+ #include <thread>
9
+ #include <functional>
10
+ #include <imx/runtime.h>
11
+
12
+ namespace imx {
13
+
14
+ // Runs \`work\` on a background thread, then calls \`on_done\` with the result.
15
+ // Calls request_frame() so the UI wakes up to display the result.
16
+ // Replace with a thread pool if you need to limit concurrency.
17
+ template<typename T>
18
+ void run_async(Runtime& runtime, std::function<T()> work, std::function<void(T)> on_done) {
19
+ std::thread([&runtime, work = std::move(work), on_done = std::move(on_done)]() {
20
+ T result = work();
21
+ on_done(std::move(result));
22
+ runtime.request_frame();
23
+ }).detach();
24
+ }
25
+
26
+ } // namespace imx
27
+ `;
28
+ const PERSISTENCE_H = `#pragma once
29
+ #include <fstream>
30
+ #include <string>
31
+ #include <imx/json.hpp>
32
+
33
+ namespace imx {
34
+
35
+ // Save state as formatted JSON. Returns true on success.
36
+ // Saves next to the executable by default — change path for platform app data dirs.
37
+ template<typename T>
38
+ bool save_json(const std::string& path, const T& state) {
39
+ std::ofstream f(path);
40
+ if (!f) return false;
41
+ f << nlohmann::json(state).dump(2);
42
+ return true;
43
+ }
44
+
45
+ // Load state from JSON file. Returns true on success, leaves state unchanged on failure.
46
+ template<typename T>
47
+ bool load_json(const std::string& path, T& state) {
48
+ std::ifstream f(path);
49
+ if (!f) return false;
50
+ nlohmann::json j = nlohmann::json::parse(f, nullptr, false);
51
+ if (j.is_discarded()) return false;
52
+ nlohmann::from_json(j, state);
53
+ return true;
54
+ }
55
+
56
+ } // namespace imx
57
+ `;
58
+ const HOTRELOAD_H = `#pragma once
59
+ #include <string>
60
+ #include <filesystem>
61
+ #include <iostream>
62
+ #include <imx/runtime.h>
63
+ #include <imgui.h>
64
+ #include "AppState.h"
65
+
66
+ #ifdef _WIN32
67
+ #ifndef WIN32_LEAN_AND_MEAN
68
+ #define WIN32_LEAN_AND_MEAN
69
+ #endif
70
+ #include <windows.h>
71
+ #else
72
+ #include <dlfcn.h>
73
+ #include <unistd.h>
74
+ #endif
75
+
76
+ struct HotModule {
77
+ using RenderFn = void(*)(imx::Runtime&, AppState&, ImGuiContext*);
78
+
79
+ #ifdef _WIN32
80
+ HMODULE handle = nullptr;
81
+ #else
82
+ void* handle = nullptr;
83
+ #endif
84
+ RenderFn render = nullptr;
85
+ std::filesystem::file_time_type last_write{};
86
+ std::string path;
87
+
88
+ bool load(const std::string& lib_path) {
89
+ path = lib_path;
90
+ if (!std::filesystem::exists(path)) {
91
+ std::cerr << "hotreload: " << path << " not found\\n";
92
+ return false;
93
+ }
94
+ last_write = std::filesystem::last_write_time(path);
95
+
96
+ #ifdef _WIN32
97
+ // Copy DLL to avoid locking the original (allows rebuild while loaded)
98
+ std::string copy_path = path + ".live";
99
+ std::filesystem::copy_file(path, copy_path, std::filesystem::copy_options::overwrite_existing);
100
+ handle = LoadLibraryA(copy_path.c_str());
101
+ if (!handle) {
102
+ std::cerr << "hotreload: LoadLibrary failed\\n";
103
+ return false;
104
+ }
105
+ render = reinterpret_cast<RenderFn>(GetProcAddress(handle, "imx_render"));
106
+ #else
107
+ handle = dlopen(path.c_str(), RTLD_NOW);
108
+ if (!handle) {
109
+ std::cerr << "hotreload: dlopen failed: " << dlerror() << "\\n";
110
+ return false;
111
+ }
112
+ render = reinterpret_cast<RenderFn>(dlsym(handle, "imx_render"));
113
+ #endif
114
+ if (!render) {
115
+ std::cerr << "hotreload: imx_render symbol not found\\n";
116
+ unload();
117
+ return false;
118
+ }
119
+ std::cout << "hotreload: loaded " << path << "\\n";
120
+ return true;
121
+ }
122
+
123
+ void unload() {
124
+ if (!handle) return;
125
+ #ifdef _WIN32
126
+ FreeLibrary(handle);
127
+ #else
128
+ dlclose(handle);
129
+ #endif
130
+ handle = nullptr;
131
+ render = nullptr;
132
+ }
133
+
134
+ bool check_reload() {
135
+ if (path.empty() || !std::filesystem::exists(path)) return false;
136
+ auto current = std::filesystem::last_write_time(path);
137
+ if (current == last_write) return false;
138
+ std::cout << "hotreload: change detected, reloading...\\n";
139
+ unload();
140
+ // Small delay to ensure file write is complete
141
+ #ifdef _WIN32
142
+ Sleep(100);
143
+ #else
144
+ usleep(100000);
145
+ #endif
146
+ if (load(path)) {
147
+ std::cout << "hotreload: reload successful\\n";
148
+ return true;
149
+ }
150
+ std::cerr << "hotreload: reload failed\\n";
151
+ return false;
152
+ }
153
+
154
+ ~HotModule() { unload(); }
155
+ };
156
+ `;
157
+ const UI_ENTRY_CPP = `#include <imx/runtime.h>
158
+ #include <imgui.h>
159
+ #include "AppState.h"
160
+
161
+ // Declared in app_root.gen.cpp (generated by imxc)
162
+ namespace imx {
163
+ template <> void render_root<AppState>(imx::Runtime& runtime, AppState& state);
164
+ }
165
+
166
+ #ifdef _WIN32
167
+ #define EXPORT __declspec(dllexport)
168
+ #else
169
+ #define EXPORT __attribute__((visibility("default")))
170
+ #endif
171
+
172
+ // ImGuiContext* must be passed from host — DLL has its own static ImGui globals
173
+ extern "C" EXPORT void imx_render(imx::Runtime& runtime, AppState& state, ImGuiContext* ctx) {
174
+ ImGui::SetCurrentContext(ctx);
175
+ imx::render_root(runtime, state);
176
+ }
177
+ `;
178
+ // ---------------------------------------------------------------------------
179
+ // Feature definitions (prefixed names to avoid collisions when combined)
180
+ // ---------------------------------------------------------------------------
181
+ export const FEATURES = [
182
+ {
183
+ name: 'async',
184
+ description: 'Background tasks with std::thread',
185
+ includes: ['#include <thread>', '#include <chrono>'],
186
+ appStateCppHeaders: ['#include <string>'],
187
+ appStateCppFields: [
188
+ ' bool loading = false;',
189
+ ' std::string result = "";',
190
+ ' std::function<void()> onFetchAsync;',
191
+ ].join('\n'),
192
+ appStateTsFields: [
193
+ ' loading: boolean;',
194
+ ' result: string;',
195
+ ' onFetchAsync: () => void;',
196
+ ].join('\n'),
197
+ callbacks: ` app.state.onFetchAsync = [&]() {
198
+ app.state.loading = true;
199
+ app.state.result = "";
200
+ imx::run_async<std::string>(
201
+ app.runtime,
202
+ []() {
203
+ // Simulate work — replace with real computation
204
+ std::this_thread::sleep_for(std::chrono::seconds(2));
205
+ return std::string("Data loaded successfully!");
206
+ },
207
+ [&](std::string res) {
208
+ app.state.result = std::move(res);
209
+ app.state.loading = false;
210
+ }
211
+ );
212
+ };`,
213
+ tsxWindow: ` <Window title="Async Demo">
214
+ <Column gap={8}>
215
+ <Text>Background Task Example</Text>
216
+ <Separator />
217
+ <Button
218
+ title="Fetch Data"
219
+ onPress={props.onFetchAsync}
220
+ disabled={props.loading}
221
+ />
222
+ {props.loading && <Text color={[1, 0.8, 0, 1]}>Loading...</Text>}
223
+ {props.result !== "" && <Text color={[0, 1, 0, 1]}>Result: {props.result}</Text>}
224
+ </Column>
225
+ </Window>`,
226
+ extraFiles: { 'async.h': ASYNC_H },
227
+ },
228
+ {
229
+ name: 'persistence',
230
+ description: 'JSON save/load with nlohmann/json',
231
+ includes: [],
232
+ appStateCppHeaders: ['#include <string>', '#include <imx/json.hpp>'],
233
+ appStateCppFields: [
234
+ ' std::string name = "World";',
235
+ ' float volume = 50.0F;',
236
+ ' bool darkMode = true;',
237
+ ' std::function<void()> onSaveState;',
238
+ ' std::function<void()> onLoadState;',
239
+ ].join('\n'),
240
+ appStateTsFields: [
241
+ ' name: string;',
242
+ ' volume: number;',
243
+ ' darkMode: boolean;',
244
+ ' onSaveState: () => void;',
245
+ ' onLoadState: () => void;',
246
+ ].join('\n'),
247
+ callbacks: ` app.state.onSaveState = [&]() {
248
+ imx::save_json("state.json", app.state);
249
+ };
250
+ app.state.onLoadState = [&]() {
251
+ imx::load_json("state.json", app.state);
252
+ app.runtime.request_frame();
253
+ };
254
+
255
+ // Auto-load saved state (silently fails if no file exists)
256
+ imx::load_json("state.json", app.state);`,
257
+ tsxWindow: ` <Window title="Persistence Demo">
258
+ <Column gap={8}>
259
+ <Text>JSON Save/Load Example</Text>
260
+ <Separator />
261
+ <TextInput label="Name" value={props.name} />
262
+ <SliderFloat label="Volume" value={props.volume} min={0} max={100} />
263
+ <Checkbox label="Dark Mode" value={props.darkMode} />
264
+ <Separator />
265
+ <Row gap={8}>
266
+ <Button title="Save" onPress={props.onSaveState} />
267
+ <Button title="Load" onPress={props.onLoadState} />
268
+ </Row>
269
+ </Column>
270
+ </Window>`,
271
+ extraFiles: { 'persistence.h': PERSISTENCE_H },
272
+ dataFields: ['name', 'volume', 'darkMode'],
273
+ },
274
+ {
275
+ name: 'networking',
276
+ description: 'HTTP client with cpp-httplib',
277
+ requires: ['async'],
278
+ includes: ['#include <thread>', '#include <imx/httplib.h>'],
279
+ appStateCppHeaders: ['#include <string>'],
280
+ appStateCppFields: [
281
+ ' std::string url = "http://jsonplaceholder.typicode.com/todos/1";',
282
+ ' std::string response = "";',
283
+ ' bool loading = false;',
284
+ ' std::function<void()> onFetchNet;',
285
+ ].join('\n'),
286
+ appStateTsFields: [
287
+ ' url: string;',
288
+ ' response: string;',
289
+ ' loading: boolean;',
290
+ ' onFetchNet: () => void;',
291
+ ].join('\n'),
292
+ callbacks: ` // Wire up the HTTP fetch callback
293
+ app.state.onFetchNet = [&]() {
294
+ app.state.loading = true;
295
+ app.state.response = "";
296
+ std::string url = app.state.url;
297
+ imx::run_async<std::string>(
298
+ app.runtime,
299
+ [url]() -> std::string {
300
+ // Parse http://host/path
301
+ auto scheme_end = url.find("://");
302
+ if (scheme_end == std::string::npos)
303
+ return "Error: invalid URL (must start with http://)";
304
+ auto host_start = scheme_end + 3;
305
+ auto path_start = url.find('/', host_start);
306
+ std::string host = (path_start != std::string::npos)
307
+ ? url.substr(0, path_start) : url;
308
+ std::string path = (path_start != std::string::npos)
309
+ ? url.substr(path_start) : "/";
310
+
311
+ // For HTTPS: #define CPPHTTPLIB_OPENSSL_SUPPORT and link OpenSSL
312
+ httplib::Client cli(host);
313
+ cli.set_connection_timeout(5);
314
+ cli.set_read_timeout(5);
315
+ auto res = cli.Get(path);
316
+ if (res) return res->body;
317
+ return "Error: request failed";
318
+ },
319
+ [&](std::string body) {
320
+ app.state.response = std::move(body);
321
+ app.state.loading = false;
322
+ }
323
+ );
324
+ };`,
325
+ tsxWindow: ` <Window title="Networking Demo">
326
+ <Column gap={8}>
327
+ <Text>HTTP Client Example</Text>
328
+ <Separator />
329
+ <TextInput label="URL" value={props.url} />
330
+ <Button title="Fetch" onPress={props.onFetchNet} disabled={props.loading} />
331
+ {props.loading && <Text color={[1, 0.8, 0, 1]}>Loading...</Text>}
332
+ {props.response !== "" && <Text wrapped={true}>{props.response}</Text>}
333
+ </Column>
334
+ </Window>`,
335
+ extraFiles: { 'async.h': ASYNC_H },
336
+ },
337
+ {
338
+ name: 'filedialog',
339
+ description: 'Native file dialogs + drag & drop',
340
+ includes: ['#include <imx/pfd.h>'],
341
+ appStateCppHeaders: ['#include <string>'],
342
+ appStateCppFields: [
343
+ ' std::string filePath = "";',
344
+ ' std::string message = "";',
345
+ ' std::function<void()> onOpenDialog;',
346
+ ' std::function<void()> onSaveDialog;',
347
+ ].join('\n'),
348
+ appStateTsFields: [
349
+ ' filePath: string;',
350
+ ' message: string;',
351
+ ' onOpenDialog: () => void;',
352
+ ' onSaveDialog: () => void;',
353
+ ].join('\n'),
354
+ callbacks: ` app.state.onOpenDialog = [&]() {
355
+ auto result = pfd::open_file("Open File").result();
356
+ if (!result.empty()) {
357
+ app.state.filePath = result[0];
358
+ app.state.message = "Opened: " + result[0];
359
+ app.runtime.request_frame();
360
+ }
361
+ };
362
+
363
+ app.state.onSaveDialog = [&]() {
364
+ auto result = pfd::save_file("Save File").result();
365
+ if (!result.empty()) {
366
+ app.state.filePath = result;
367
+ app.state.message = "Save to: " + result;
368
+ app.runtime.request_frame();
369
+ }
370
+ };
371
+
372
+ // GLFW file drop callback
373
+ glfwSetDropCallback(window, [](GLFWwindow* w, int count, const char** paths) {
374
+ auto* a = static_cast<App*>(glfwGetWindowUserPointer(w));
375
+ if (a && count > 0) {
376
+ a->state.filePath = paths[0];
377
+ a->state.message = "Dropped: " + std::string(paths[0]);
378
+ a->runtime.request_frame();
379
+ }
380
+ });`,
381
+ tsxWindow: ` <Window title="File Dialog Demo">
382
+ <Column gap={8}>
383
+ <Text>Native File Dialogs + Drag & Drop</Text>
384
+ <Separator />
385
+ <Row gap={8}>
386
+ <Button title="Open File" onPress={props.onOpenDialog} />
387
+ <Button title="Save File" onPress={props.onSaveDialog} />
388
+ </Row>
389
+ <Text>Drag & drop a file onto this window</Text>
390
+ {props.message !== "" && <Text color={[0, 1, 0, 1]}>{props.message}</Text>}
391
+ {props.filePath !== "" && <Text>Path: {props.filePath}</Text>}
392
+ </Column>
393
+ </Window>`,
394
+ extraFiles: {},
395
+ },
396
+ {
397
+ name: 'hotreload',
398
+ description: 'DLL hot reload for live UI iteration',
399
+ includes: [],
400
+ appStateCppHeaders: ['#include <string>'],
401
+ appStateCppFields: ' std::string watchCmd;\n std::function<void()> onCopyCmd;',
402
+ appStateTsFields: ' watchCmd: string;\n onCopyCmd: () => void;',
403
+ callbacks: ` app.state.watchCmd = "npx imxc watch src -o build/generated --build \\"cmake --build build --target imx_ui\\"";
404
+ app.state.onCopyCmd = [&]() { imx::clipboard_set(app.state.watchCmd.c_str()); };`,
405
+ tsxWindow: ` <Window title="Hot Reload">
406
+ <Column gap={8}>
407
+ <Text>Run this in a second terminal to enable live reload:</Text>
408
+ <Text wrapped={true}>{props.watchCmd}</Text>
409
+ <Button title="Copy" onPress={props.onCopyCmd} />
410
+ </Column>
411
+ </Window>`,
412
+ extraFiles: {},
413
+ },
414
+ ];
415
+ // ---------------------------------------------------------------------------
416
+ // CMake for hot reload (host exe + UI shared lib)
417
+ // ---------------------------------------------------------------------------
418
+ function cmakeHotreloadCombined(projectName) {
419
+ return `cmake_minimum_required(VERSION 3.25)
420
+ project(${projectName} LANGUAGES CXX)
421
+
422
+ set(CMAKE_CXX_STANDARD 20)
423
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
424
+
425
+ include(FetchContent)
426
+ set(FETCHCONTENT_QUIET OFF)
427
+
428
+ FetchContent_Declare(
429
+ imx
430
+ GIT_REPOSITORY https://github.com/bgocumlu/imx.git
431
+ GIT_TAG v0.6.0
432
+ GIT_SHALLOW TRUE
433
+ GIT_PROGRESS TRUE
434
+ )
435
+ message(STATUS "Fetching IMX (includes ImGui + GLFW)...")
436
+ FetchContent_MakeAvailable(imx)
437
+
438
+ include(ImxCompile)
439
+
440
+ imx_compile_tsx(GENERATED
441
+ SOURCES src/App.tsx
442
+ OUTPUT_DIR \${CMAKE_BINARY_DIR}/generated
443
+ )
444
+
445
+ # UI shared library (hot-reloadable)
446
+ add_library(imx_ui SHARED
447
+ src/ui_entry.cpp
448
+ \${GENERATED}
449
+ )
450
+ target_link_libraries(imx_ui PRIVATE imx::renderer)
451
+ target_include_directories(imx_ui PRIVATE \${CMAKE_BINARY_DIR}/generated \${CMAKE_CURRENT_SOURCE_DIR}/src)
452
+
453
+ # Host executable (loads UI module dynamically)
454
+ add_executable(${projectName}
455
+ src/main.cpp
456
+ )
457
+ set_target_properties(${projectName} PROPERTIES WIN32_EXECUTABLE $<CONFIG:Release>)
458
+ target_link_libraries(${projectName} PRIVATE imx::renderer \${CMAKE_DL_LIBS})
459
+ target_include_directories(${projectName} PRIVATE \${CMAKE_CURRENT_SOURCE_DIR}/src)
460
+
461
+ # Copy DLL/SO next to host exe after build
462
+ add_custom_command(TARGET imx_ui POST_BUILD
463
+ COMMAND \${CMAKE_COMMAND} -E copy $<TARGET_FILE:imx_ui> $<TARGET_FILE_DIR:${projectName}>
464
+ COMMENT "Copying UI module to host directory"
465
+ )
466
+ add_dependencies(${projectName} imx_ui)
467
+
468
+ # Copy public/ assets to output directory
469
+ add_custom_command(TARGET ${projectName} POST_BUILD
470
+ COMMAND \${CMAKE_COMMAND} -E copy_directory
471
+ \${CMAKE_CURRENT_SOURCE_DIR}/public
472
+ $<TARGET_FILE_DIR:${projectName}>
473
+ COMMENT "Copying public/ assets"
474
+ )
475
+ `;
476
+ }
477
+ // ---------------------------------------------------------------------------
478
+ // Base main.cpp template with placeholders
479
+ // ---------------------------------------------------------------------------
480
+ function buildMainCpp(projectName, extraIncludes, callbacks, hasPersistence) {
481
+ const includeBlock = extraIncludes ? '\n' + extraIncludes : '';
482
+ const persistenceInclude = hasPersistence ? '\n#include "persistence.h"' : '';
483
+ return `#include <imx/runtime.h>
484
+ #include <imx/renderer.h>
485
+
486
+ #include <imgui.h>
487
+ #include <imgui_impl_glfw.h>
488
+ #include <imgui_impl_opengl3.h>
489
+ #include <GLFW/glfw3.h>
490
+ ${includeBlock}${persistenceInclude}
491
+ #include "AppState.h"
492
+
493
+ struct App {
494
+ GLFWwindow* window = nullptr;
495
+ ImGuiIO* io = nullptr;
496
+ imx::Runtime runtime;
497
+ AppState state;
498
+ };
499
+
500
+ static void render_frame(App& app) {
501
+ glfwMakeContextCurrent(app.window);
502
+ if (glfwGetWindowAttrib(app.window, GLFW_ICONIFIED) != 0) return;
503
+
504
+ int fb_w = 0, fb_h = 0;
505
+ glfwGetFramebufferSize(app.window, &fb_w, &fb_h);
506
+ if (fb_w <= 0 || fb_h <= 0) return;
507
+
508
+ ImGui_ImplOpenGL3_NewFrame();
509
+ ImGui_ImplGlfw_NewFrame();
510
+ ImGui::NewFrame();
511
+
512
+ imx::render_root(app.runtime, app.state);
513
+
514
+ ImGui::Render();
515
+ glViewport(0, 0, fb_w, fb_h);
516
+ glClearColor(0.12F, 0.12F, 0.15F, 1.0F);
517
+ glClear(GL_COLOR_BUFFER_BIT);
518
+ ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
519
+
520
+ if ((app.io->ConfigFlags & ImGuiConfigFlags_ViewportsEnable) != 0) {
521
+ ImGui::UpdatePlatformWindows();
522
+ ImGui::RenderPlatformWindowsDefault();
523
+ }
524
+
525
+ glfwMakeContextCurrent(app.window);
526
+ glfwSwapBuffers(app.window);
527
+ }
528
+
529
+ static void window_size_callback(GLFWwindow* window, int, int) {
530
+ auto* app = static_cast<App*>(glfwGetWindowUserPointer(window));
531
+ if (app) render_frame(*app);
532
+ }
533
+
534
+ int main() {
535
+ if (glfwInit() == 0) return 1;
536
+
537
+ const char* glsl_version = "#version 150";
538
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
539
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
540
+ glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
541
+ #ifdef __APPLE__
542
+ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
543
+ #endif
544
+
545
+ GLFWwindow* window = glfwCreateWindow(800, 600, "${projectName}", nullptr, nullptr);
546
+ if (!window) { glfwTerminate(); return 1; }
547
+ glfwMakeContextCurrent(window);
548
+ glfwSwapInterval(1);
549
+
550
+ IMGUI_CHECKVERSION();
551
+ ImGui::CreateContext();
552
+ ImGuiIO& io = ImGui::GetIO();
553
+ io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
554
+ io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
555
+
556
+ ImGui::StyleColorsDark();
557
+ ImGuiStyle& style = ImGui::GetStyle();
558
+ if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
559
+ style.WindowRounding = 0.0F;
560
+ style.Colors[ImGuiCol_WindowBg].w = 1.0F;
561
+ }
562
+
563
+ ImGui_ImplGlfw_InitForOpenGL(window, true);
564
+ ImGui_ImplOpenGL3_Init(glsl_version);
565
+
566
+ App app;
567
+ app.window = window;
568
+ app.io = &io;
569
+ glfwSetWindowUserPointer(window, &app);
570
+ glfwSetWindowSizeCallback(window, window_size_callback);
571
+
572
+ ${callbacks}
573
+
574
+ while (glfwWindowShouldClose(window) == 0) {
575
+ if (app.runtime.needs_frame()) {
576
+ glfwPollEvents();
577
+ } else {
578
+ glfwWaitEventsTimeout(0.1);
579
+ }
580
+ render_frame(app);
581
+ app.runtime.frame_rendered(ImGui::IsAnyItemActive());
582
+ }
583
+
584
+ ImGui_ImplOpenGL3_Shutdown();
585
+ ImGui_ImplGlfw_Shutdown();
586
+ ImGui::DestroyContext();
587
+ glfwDestroyWindow(window);
588
+ glfwTerminate();
589
+ return 0;
590
+ }
591
+
592
+ #ifdef _WIN32
593
+ #include <windows.h>
594
+ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { return main(); }
595
+ #endif
596
+ `;
597
+ }
598
+ // ---------------------------------------------------------------------------
599
+ // Hot reload main.cpp template (DLL-loading variant)
600
+ // ---------------------------------------------------------------------------
601
+ function buildMainCppHotreload(projectName, extraIncludes, callbacks, hasPersistence) {
602
+ const includeBlock = extraIncludes ? '\n' + extraIncludes : '';
603
+ const persistenceInclude = hasPersistence ? '\n#include "persistence.h"' : '';
604
+ return `#include <imx/runtime.h>
605
+ #include <imx/renderer.h>
606
+
607
+ #include <imgui.h>
608
+ #include <imgui_impl_glfw.h>
609
+ #include <imgui_impl_opengl3.h>
610
+ #include <GLFW/glfw3.h>
611
+ ${includeBlock}${persistenceInclude}
612
+ #include "hotreload.h"
613
+ #include "AppState.h"
614
+
615
+ struct App {
616
+ GLFWwindow* window = nullptr;
617
+ ImGuiIO* io = nullptr;
618
+ imx::Runtime runtime;
619
+ AppState state;
620
+ HotModule module;
621
+ };
622
+
623
+ static void render_frame(App& app) {
624
+ glfwMakeContextCurrent(app.window);
625
+ if (glfwGetWindowAttrib(app.window, GLFW_ICONIFIED) != 0) return;
626
+
627
+ int fb_w = 0, fb_h = 0;
628
+ glfwGetFramebufferSize(app.window, &fb_w, &fb_h);
629
+ if (fb_w <= 0 || fb_h <= 0) return;
630
+
631
+ ImGui_ImplOpenGL3_NewFrame();
632
+ ImGui_ImplGlfw_NewFrame();
633
+ ImGui::NewFrame();
634
+
635
+ app.module.check_reload();
636
+ if (app.module.render) {
637
+ app.module.render(app.runtime, app.state, ImGui::GetCurrentContext());
638
+ }
639
+
640
+ ImGui::Render();
641
+ glViewport(0, 0, fb_w, fb_h);
642
+ glClearColor(0.12F, 0.12F, 0.15F, 1.0F);
643
+ glClear(GL_COLOR_BUFFER_BIT);
644
+ ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
645
+
646
+ if ((app.io->ConfigFlags & ImGuiConfigFlags_ViewportsEnable) != 0) {
647
+ ImGui::UpdatePlatformWindows();
648
+ ImGui::RenderPlatformWindowsDefault();
649
+ }
650
+
651
+ glfwMakeContextCurrent(app.window);
652
+ glfwSwapBuffers(app.window);
653
+ }
654
+
655
+ static void window_size_callback(GLFWwindow* window, int, int) {
656
+ auto* app = static_cast<App*>(glfwGetWindowUserPointer(window));
657
+ if (app) render_frame(*app);
658
+ }
659
+
660
+ int main() {
661
+ if (glfwInit() == 0) return 1;
662
+
663
+ const char* glsl_version = "#version 150";
664
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
665
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
666
+ glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
667
+ #ifdef __APPLE__
668
+ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
669
+ #endif
670
+
671
+ GLFWwindow* window = glfwCreateWindow(800, 600, "${projectName}", nullptr, nullptr);
672
+ if (!window) { glfwTerminate(); return 1; }
673
+ glfwMakeContextCurrent(window);
674
+ glfwSwapInterval(1);
675
+
676
+ IMGUI_CHECKVERSION();
677
+ ImGui::CreateContext();
678
+ ImGuiIO& io = ImGui::GetIO();
679
+ io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
680
+ io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
681
+
682
+ ImGui::StyleColorsDark();
683
+ ImGuiStyle& style = ImGui::GetStyle();
684
+ if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
685
+ style.WindowRounding = 0.0F;
686
+ style.Colors[ImGuiCol_WindowBg].w = 1.0F;
687
+ }
688
+
689
+ ImGui_ImplGlfw_InitForOpenGL(window, true);
690
+ ImGui_ImplOpenGL3_Init(glsl_version);
691
+
692
+ App app;
693
+ app.window = window;
694
+ app.io = &io;
695
+ glfwSetWindowUserPointer(window, &app);
696
+ glfwSetWindowSizeCallback(window, window_size_callback);
697
+
698
+ ${callbacks}
699
+
700
+ #ifdef _WIN32
701
+ app.module.load("imx_ui.dll");
702
+ #elif defined(__APPLE__)
703
+ app.module.load("libimx_ui.dylib");
704
+ #else
705
+ app.module.load("libimx_ui.so");
706
+ #endif
707
+
708
+ while (glfwWindowShouldClose(window) == 0) {
709
+ if (app.runtime.needs_frame()) {
710
+ glfwPollEvents();
711
+ } else {
712
+ glfwWaitEventsTimeout(0.1);
713
+ }
714
+ render_frame(app);
715
+ app.runtime.frame_rendered(ImGui::IsAnyItemActive());
716
+ }
717
+
718
+ app.module.unload();
719
+
720
+ ImGui_ImplOpenGL3_Shutdown();
721
+ ImGui_ImplGlfw_Shutdown();
722
+ ImGui::DestroyContext();
723
+ glfwDestroyWindow(window);
724
+ glfwTerminate();
725
+ return 0;
726
+ }
727
+
728
+ #ifdef _WIN32
729
+ #include <windows.h>
730
+ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { return main(); }
731
+ #endif
732
+ `;
733
+ }
734
+ // ---------------------------------------------------------------------------
735
+ // AppState.h builder
736
+ // ---------------------------------------------------------------------------
737
+ function buildAppStateH(headers, fields, nlohmannMacroFields) {
738
+ const dedupedHeaders = [...new Set(['#include <functional>', ...headers])];
739
+ const headerBlock = dedupedHeaders.join('\n');
740
+ const macroLine = nlohmannMacroFields
741
+ ? `\n// Only serialize data fields — callbacks are not persisted\nNLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(AppState, ${nlohmannMacroFields.join(', ')})\n`
742
+ : '';
743
+ return `#pragma once
744
+ ${headerBlock}
745
+
746
+ struct AppState {
747
+ ${fields}
748
+ };
749
+ ${macroLine}`;
750
+ }
751
+ // ---------------------------------------------------------------------------
752
+ // App.tsx builder
753
+ // ---------------------------------------------------------------------------
754
+ function buildAppTsx(windows) {
755
+ return `export default function App(props: AppState) {
756
+ return (
757
+ <DockSpace>
758
+ ${windows}
759
+ </DockSpace>
760
+ );
761
+ }
762
+ `;
763
+ }
764
+ // ---------------------------------------------------------------------------
765
+ // Deduplicate C++ struct fields by field name
766
+ // ---------------------------------------------------------------------------
767
+ function deduplicateFields(fieldsBlock) {
768
+ const seen = new Set();
769
+ const result = [];
770
+ for (const line of fieldsBlock.split('\n')) {
771
+ const trimmed = line.trim();
772
+ if (trimmed === '')
773
+ continue;
774
+ // Extract field name: match "type name = ..." or "std::function<...> name;"
775
+ const match = trimmed.match(/(\w+)\s*[=;]/);
776
+ if (match) {
777
+ const fieldName = match[1];
778
+ if (seen.has(fieldName))
779
+ continue;
780
+ seen.add(fieldName);
781
+ }
782
+ result.push(line);
783
+ }
784
+ return result.join('\n');
785
+ }
786
+ // ---------------------------------------------------------------------------
787
+ // Deduplicate TS interface fields by field name
788
+ // ---------------------------------------------------------------------------
789
+ function deduplicateTsFields(fieldsBlock) {
790
+ const seen = new Set();
791
+ const result = [];
792
+ for (const line of fieldsBlock.split('\n')) {
793
+ const trimmed = line.trim();
794
+ if (trimmed === '')
795
+ continue;
796
+ // Extract field name: "name: type;"
797
+ const match = trimmed.match(/^(\w+)\s*[?]?\s*:/);
798
+ if (match) {
799
+ const fieldName = match[1];
800
+ if (seen.has(fieldName))
801
+ continue;
802
+ seen.add(fieldName);
803
+ }
804
+ result.push(line);
805
+ }
806
+ return result.join('\n');
807
+ }
808
+ // ---------------------------------------------------------------------------
809
+ // Main entry: generateCombined()
810
+ // ---------------------------------------------------------------------------
811
+ export function generateCombined(featureNames, projectDir, projectName) {
812
+ // 1. Resolve feature objects
813
+ const featureMap = new Map();
814
+ for (const f of FEATURES)
815
+ featureMap.set(f.name, f);
816
+ // Validate all names exist
817
+ for (const name of featureNames) {
818
+ if (!featureMap.has(name)) {
819
+ console.error(`Error: unknown feature "${name}". Available: ${FEATURES.map(f => f.name).join(', ')}`);
820
+ process.exit(1);
821
+ }
822
+ }
823
+ // 2. Resolve dependencies (auto-add required features)
824
+ const selected = new Set(featureNames);
825
+ let changed = true;
826
+ while (changed) {
827
+ changed = false;
828
+ for (const name of [...selected]) {
829
+ const feat = featureMap.get(name);
830
+ if (feat.requires) {
831
+ for (const dep of feat.requires) {
832
+ if (!selected.has(dep)) {
833
+ selected.add(dep);
834
+ changed = true;
835
+ }
836
+ }
837
+ }
838
+ }
839
+ }
840
+ // Deterministic order: follow FEATURES array order
841
+ const ordered = FEATURES.filter(f => selected.has(f.name));
842
+ // 4. Collect everything
843
+ const allIncludes = [];
844
+ const allCppFields = [];
845
+ const allCppHeaders = [];
846
+ const allTsFields = [];
847
+ const allCallbacks = [];
848
+ const allWindows = [];
849
+ const allExtraFiles = {};
850
+ let nlohmannFields = null;
851
+ for (const feat of ordered) {
852
+ for (const inc of feat.includes) {
853
+ if (!allIncludes.includes(inc))
854
+ allIncludes.push(inc);
855
+ }
856
+ if (feat.appStateCppFields)
857
+ allCppFields.push(feat.appStateCppFields);
858
+ for (const h of feat.appStateCppHeaders) {
859
+ if (!allCppHeaders.includes(h))
860
+ allCppHeaders.push(h);
861
+ }
862
+ if (feat.appStateTsFields)
863
+ allTsFields.push(feat.appStateTsFields);
864
+ if (feat.callbacks)
865
+ allCallbacks.push(feat.callbacks);
866
+ if (feat.tsxWindow)
867
+ allWindows.push(feat.tsxWindow);
868
+ Object.assign(allExtraFiles, feat.extraFiles);
869
+ if (feat.dataFields) {
870
+ nlohmannFields = nlohmannFields || [];
871
+ nlohmannFields.push(...feat.dataFields);
872
+ }
873
+ }
874
+ // Deduplicate struct fields (e.g. `loading` from both async and networking)
875
+ const mergedCppFields = deduplicateFields(allCppFields.join('\n'));
876
+ const mergedTsFields = deduplicateTsFields(allTsFields.join('\n'));
877
+ // 5. Check for existing files
878
+ const srcDir = path.join(projectDir, 'src');
879
+ if (fs.existsSync(path.join(srcDir, 'App.tsx'))) {
880
+ console.error(`Error: ${srcDir}/App.tsx already exists. Aborting.`);
881
+ process.exit(1);
882
+ }
883
+ fs.mkdirSync(srcDir, { recursive: true });
884
+ const publicDir = path.join(projectDir, 'public');
885
+ fs.mkdirSync(publicDir, { recursive: true });
886
+ // 6. Build files
887
+ const hasHotreload = selected.has('hotreload');
888
+ const hasPersistence = selected.has('persistence');
889
+ const includeLines = allIncludes.length > 0 ? allIncludes.join('\n') : '';
890
+ // Add async.h include if async.h is in extra files
891
+ const hasAsyncH = 'async.h' in allExtraFiles;
892
+ const asyncInclude = hasAsyncH ? '#include "async.h"' : '';
893
+ const fullIncludes = [includeLines, asyncInclude].filter(Boolean).join('\n');
894
+ // Build main.cpp
895
+ const mainCpp = hasHotreload
896
+ ? buildMainCppHotreload(projectName, fullIncludes, allCallbacks.join('\n\n'), hasPersistence)
897
+ : buildMainCpp(projectName, fullIncludes, allCallbacks.join('\n\n'), hasPersistence);
898
+ const appStateH = buildAppStateH(allCppHeaders, mergedCppFields, nlohmannFields);
899
+ const appStateInterface = `interface AppState {\n${mergedTsFields}\n}`;
900
+ const appTsx = buildAppTsx(allWindows.join('\n'));
901
+ const imxDts = buildImxDts(appStateInterface);
902
+ // Build CMakeLists
903
+ const cmake = hasHotreload
904
+ ? cmakeHotreloadCombined(projectName)
905
+ : cmakeTemplate(projectName, 'https://github.com/bgocumlu/imx.git');
906
+ // 7. Write files
907
+ fs.writeFileSync(path.join(srcDir, 'main.cpp'), mainCpp);
908
+ fs.writeFileSync(path.join(srcDir, 'AppState.h'), appStateH);
909
+ fs.writeFileSync(path.join(srcDir, 'App.tsx'), appTsx);
910
+ fs.writeFileSync(path.join(srcDir, 'imx.d.ts'), imxDts);
911
+ fs.writeFileSync(path.join(projectDir, 'tsconfig.json'), TSCONFIG);
912
+ fs.writeFileSync(path.join(projectDir, 'CMakeLists.txt'), cmake);
913
+ fs.writeFileSync(path.join(projectDir, '.gitignore'), GITIGNORE);
914
+ const createdFiles = [
915
+ 'src/main.cpp',
916
+ 'src/AppState.h',
917
+ 'src/App.tsx',
918
+ 'src/imx.d.ts',
919
+ ];
920
+ for (const [filename, content] of Object.entries(allExtraFiles)) {
921
+ fs.writeFileSync(path.join(srcDir, filename), content);
922
+ createdFiles.push(`src/${filename}`);
923
+ }
924
+ // Write hotreload-specific files
925
+ if (hasHotreload) {
926
+ fs.writeFileSync(path.join(srcDir, 'hotreload.h'), HOTRELOAD_H);
927
+ fs.writeFileSync(path.join(srcDir, 'ui_entry.cpp'), UI_ENTRY_CPP);
928
+ createdFiles.push('src/hotreload.h', 'src/ui_entry.cpp');
929
+ }
930
+ createdFiles.push('tsconfig.json', 'CMakeLists.txt', '.gitignore', 'public/');
931
+ // 8. Print summary
932
+ const featureList = ordered.map(f => f.name).join(', ');
933
+ console.log(`imxc: initialized project "${projectName}" with features: ${featureList}`);
934
+ console.log('');
935
+ console.log(' Created:');
936
+ for (const file of createdFiles) {
937
+ const pad = file.length < 22 ? ' '.repeat(22 - file.length) : ' ';
938
+ console.log(` ${file}${pad}`);
939
+ }
940
+ console.log('');
941
+ console.log(' Next steps:');
942
+ console.log(` cd ${projectName}`);
943
+ console.log(` cmake -B build`);
944
+ console.log(` cmake --build build`);
945
+ }