#include #include #include #include #include #include #include #include #include #include "pxl8_macros.h" #include "pxl8_script.h" #include "pxl8_ui.h" struct pxl8_script { lua_State* L; pxl8_gfx* gfx; pxl8_input_state* input; pxl8_ui* ui; char last_error[1024]; char main_path[256]; char watch_dir[256]; time_t latest_mod_time; }; #define PXL8_MAX_REPL_COMMAND_SIZE 4096 static void pxl8_script_repl_promote_locals(const char* input, char* output, size_t output_size) { size_t i = 0; size_t j = 0; bool in_string = false; bool in_comment = false; while (input[i] && j < output_size - 1) { if (in_comment) { output[j++] = input[i]; if (input[i] == '\n') { in_comment = false; } i++; continue; } if (input[i] == ';' && !in_string) { in_comment = true; output[j++] = input[i++]; continue; } if (input[i] == '"' && (i == 0 || input[i-1] != '\\')) { in_string = !in_string; output[j++] = input[i++]; continue; } if (!in_string && input[i] == '(' && strncmp(&input[i], "(local ", 7) == 0) { strncpy(&output[j], "(global ", 8); j += 8; i += 7; continue; } output[j++] = input[i++]; } output[j] = '\0'; } static const char* pxl8_ffi_cdefs = "typedef uint8_t u8;\n" "typedef uint16_t u16;\n" "typedef uint32_t u32;\n" "typedef uint64_t u64;\n" "typedef int8_t i8;\n" "typedef int16_t i16;\n" "typedef int32_t i32;\n" "typedef int64_t i64;\n" "typedef float f32;\n" "typedef double f64;\n" "typedef struct pxl8_gfx pxl8_gfx;\n" "typedef struct { int x, y, w, h; } pxl8_bounds;\n" "typedef struct { int x, y; } pxl8_point;\n" "\n" "i32 pxl8_gfx_get_height(pxl8_gfx* ctx);\n" "i32 pxl8_gfx_get_width(pxl8_gfx* ctx);\n" "void pxl8_circle(pxl8_gfx* ctx, i32 x, i32 y, i32 r, u32 color);\n" "void pxl8_circle_fill(pxl8_gfx* ctx, i32 x, i32 y, i32 r, u32 color);\n" "void pxl8_clr(pxl8_gfx* ctx, u32 color);\n" "u32 pxl8_get_pixel(pxl8_gfx* ctx, i32 x, i32 y);\n" "void pxl8_line(pxl8_gfx* ctx, i32 x0, i32 y0, i32 x1, i32 y1, u32 color);\n" "void pxl8_pixel(pxl8_gfx* ctx, i32 x, i32 y, u32 color);\n" "void pxl8_rect(pxl8_gfx* ctx, i32 x, i32 y, i32 w, i32 h, u32 color);\n" "void pxl8_rect_fill(pxl8_gfx* ctx, i32 x, i32 y, i32 w, i32 h, u32 color);\n" "void pxl8_sprite(pxl8_gfx* ctx, i32 id, i32 x, i32 y, i32 w, i32 h, bool flip_x, bool flip_y);\n" "void pxl8_text(pxl8_gfx* ctx, const char* str, i32 x, i32 y, u32 color);\n" "void pxl8_gfx_color_ramp(pxl8_gfx* ctx, u8 start, u8 count, u32 from_color, u32 to_color);\n" "void pxl8_gfx_fade_palette(pxl8_gfx* ctx, u8 start, u8 count, f32 amount, u32 target_color);\n" "i32 pxl8_gfx_load_palette(pxl8_gfx* ctx, const char* filepath);\n" "i32 pxl8_gfx_load_sprite(pxl8_gfx* ctx, const char* filepath, u32* sprite_id);\n" "i32 pxl8_gfx_create_texture(pxl8_gfx* ctx, const u8* pixels, u32 width, u32 height);\n" "void pxl8_gfx_upload_atlas(pxl8_gfx* ctx);\n" "typedef struct pxl8_input_state pxl8_input_state;\n" "bool pxl8_key_down(const pxl8_input_state* input, const char* key_name);\n" "bool pxl8_key_pressed(const pxl8_input_state* input, const char* key_name);\n" "bool pxl8_key_released(const pxl8_input_state* input, const char* key_name);\n" "int pxl8_mouse_wheel_x(const pxl8_input_state* input);\n" "int pxl8_mouse_wheel_y(const pxl8_input_state* input);\n" "int pxl8_mouse_x(const pxl8_input_state* input);\n" "int pxl8_mouse_y(const pxl8_input_state* input);\n" "void pxl8_lua_debug(const char* msg);\n" "void pxl8_lua_error(const char* msg);\n" "void pxl8_lua_info(const char* msg);\n" "void pxl8_lua_trace(const char* msg);\n" "void pxl8_lua_warn(const char* msg);\n" "\n" "typedef u32 pxl8_tile;\n" "typedef struct pxl8_tilemap pxl8_tilemap;\n" "typedef struct pxl8_tilesheet pxl8_tilesheet;\n" "pxl8_tilemap* pxl8_tilemap_create(u32 width, u32 height, u32 tile_size);\n" "void pxl8_tilemap_destroy(pxl8_tilemap* tilemap);\n" "u32 pxl8_tilemap_get_height(const pxl8_tilemap* tilemap);\n" "pxl8_tile pxl8_tilemap_get_tile(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y);\n" "u32 pxl8_tilemap_get_tile_size(const pxl8_tilemap* tilemap);\n" "void* pxl8_tilemap_get_tile_user_data(const pxl8_tilemap* tilemap, u16 tile_id);\n" "u32 pxl8_tilemap_get_width(const pxl8_tilemap* tilemap);\n" "void pxl8_tilemap_set_tile_user_data(pxl8_tilemap* tilemap, u16 tile_id, void* user_data);\n" "bool pxl8_tilemap_check_collision(const pxl8_tilemap* tilemap, i32 x, i32 y, i32 w, i32 h);\n" "u16 pxl8_tilemap_get_tile_id(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y);\n" "bool pxl8_tilemap_is_solid(const pxl8_tilemap* tilemap, u32 x, u32 y);\n" "void pxl8_tilemap_render(const pxl8_tilemap* tilemap, pxl8_gfx* gfx);\n" "void pxl8_tilemap_render_layer(const pxl8_tilemap* tilemap, pxl8_gfx* gfx, u32 layer);\n" "void pxl8_tilemap_set_camera(pxl8_tilemap* tilemap, i32 x, i32 y);\n" "void pxl8_tilemap_set_tile(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u16 tile_id, u8 flags);\n" "i32 pxl8_tilemap_set_tilesheet(pxl8_tilemap* tilemap, pxl8_tilesheet* tilesheet);\n" "i32 pxl8_tilemap_load_ase(pxl8_tilemap* tilemap, const char* filepath, u32 layer);\n" "pxl8_tilesheet* pxl8_tilesheet_create(u32 tile_size);\n" "void pxl8_tilesheet_destroy(pxl8_tilesheet* tilesheet);\n" "i32 pxl8_tilesheet_load(pxl8_tilesheet* tilesheet, const char* filepath, pxl8_gfx* gfx);\n" "\n" "typedef struct {\n" " float angle;\n" " float ax, ay, az;\n" " unsigned int color;\n" " unsigned int end_color;\n" " unsigned char flags;\n" " float life;\n" " float max_life;\n" " float size;\n" " float spin;\n" " unsigned int start_color;\n" " float vx, vy, vz;\n" " float x, y, z;\n" "} pxl8_particle;\n" "\n" "typedef struct {\n" " float amplitude;\n" " float base_y;\n" " unsigned int color;\n" " unsigned int fade_color;\n" " int height;\n" " float phase;\n" " float speed;\n" "} pxl8_raster_bar;\n" "\n" "typedef struct pxl8_particles pxl8_particles;\n" "pxl8_particles* pxl8_particles_create(u32 max_count);\n" "void pxl8_particles_destroy(pxl8_particles* particles);\n" "void pxl8_particles_clear(pxl8_particles* particles);\n" "void pxl8_particles_emit(pxl8_particles* particles, u32 count);\n" "void pxl8_particles_render(pxl8_particles* particles, pxl8_gfx* gfx);\n" "void pxl8_particles_update(pxl8_particles* particles, float dt);\n" "void pxl8_vfx_explosion(pxl8_particles* particles, int x, int y, unsigned int color, float force);\n" "void pxl8_vfx_fire(pxl8_particles* particles, int x, int y, int width, unsigned char palette_start);\n" "void pxl8_vfx_plasma(pxl8_gfx* ctx, f32 time, f32 scale1, f32 scale2, u8 palette_offset);\n" "void pxl8_vfx_rain(pxl8_particles* particles, int width, float wind);\n" "void pxl8_vfx_raster_bars(pxl8_gfx* ctx, pxl8_raster_bar* bars, u32 bar_count, f32 time);\n" "void pxl8_vfx_rotozoom(pxl8_gfx* ctx, f32 angle, f32 zoom, i32 cx, i32 cy);\n" "void pxl8_vfx_smoke(pxl8_particles* particles, int x, int y, unsigned char color);\n" "void pxl8_vfx_snow(pxl8_particles* particles, int width, float wind);\n" "void pxl8_vfx_sparks(pxl8_particles* particles, int x, int y, unsigned int color);\n" "void pxl8_vfx_starfield(pxl8_particles* particles, float speed, float spread);\n" "void pxl8_vfx_tunnel(pxl8_gfx* ctx, f32 time, f32 speed, f32 twist);\n" "void pxl8_vfx_water_ripple(pxl8_gfx* ctx, f32* height_map, i32 drop_x, i32 drop_y, f32 damping);\n" "\n" "typedef struct { float x, y, z; } pxl8_vec3;\n" "typedef struct { float x, y, z, w; } pxl8_vec4;\n" "typedef struct { float m[16]; } pxl8_mat4;\n" "\n" "void pxl8_3d_clear_zbuffer(pxl8_gfx* gfx);\n" "void pxl8_3d_draw_line_3d(pxl8_gfx* gfx, pxl8_vec3 p0, pxl8_vec3 p1, u32 color);\n" "void pxl8_3d_draw_triangle_raw(pxl8_gfx* gfx, pxl8_vec3 v0, pxl8_vec3 v1, pxl8_vec3 v2, u32 color);\n" "void pxl8_3d_draw_triangle_textured(pxl8_gfx* gfx, pxl8_vec3 v0, pxl8_vec3 v1, pxl8_vec3 v2, f32 u0, f32 v0, f32 u1, f32 v1, f32 u2, f32 v2, u32 texture_id);\n" "void pxl8_3d_set_affine_textures(pxl8_gfx* gfx, bool affine);\n" "void pxl8_3d_set_backface_culling(pxl8_gfx* gfx, bool culling);\n" "void pxl8_3d_set_model(pxl8_gfx* gfx, pxl8_mat4 mat);\n" "void pxl8_3d_set_projection(pxl8_gfx* gfx, pxl8_mat4 mat);\n" "void pxl8_3d_set_view(pxl8_gfx* gfx, pxl8_mat4 mat);\n" "void pxl8_3d_set_wireframe(pxl8_gfx* gfx, bool wireframe);\n" "pxl8_mat4 pxl8_mat4_identity(void);\n" "pxl8_mat4 pxl8_mat4_lookat(pxl8_vec3 eye, pxl8_vec3 center, pxl8_vec3 up);\n" "pxl8_mat4 pxl8_mat4_multiply(pxl8_mat4 a, pxl8_mat4 b);\n" "pxl8_mat4 pxl8_mat4_ortho(float left, float right, float bottom, float top, float near, float far);\n" "pxl8_mat4 pxl8_mat4_perspective(float fov, float aspect, float near, float far);\n" "pxl8_mat4 pxl8_mat4_rotate_x(float angle);\n" "pxl8_mat4 pxl8_mat4_rotate_y(float angle);\n" "pxl8_mat4 pxl8_mat4_rotate_z(float angle);\n" "pxl8_mat4 pxl8_mat4_scale(float x, float y, float z);\n" "pxl8_mat4 pxl8_mat4_translate(float x, float y, float z);\n" "\n" "typedef struct pxl8_world pxl8_world;\n" "pxl8_world* pxl8_world_create(void);\n" "void pxl8_world_destroy(pxl8_world* world);\n" "int pxl8_world_load(pxl8_world* world, const char* path);\n" "void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos);\n" "void pxl8_world_unload(pxl8_world* world);\n" "bool pxl8_world_is_loaded(const pxl8_world* world);\n" "\n" "typedef struct pxl8_ui pxl8_ui;\n" "typedef struct { unsigned char bg_color; unsigned int sprite_id; int corner_size; int edge_size; int padding; } pxl8_frame_theme;\n" "typedef struct { bool enabled; const char* label; } pxl8_menu_item;\n" "pxl8_ui* pxl8_ui_create(pxl8_gfx* gfx);\n" "void pxl8_ui_destroy(pxl8_ui* ui);\n" "void pxl8_ui_frame_begin(pxl8_ui* ui);\n" "void pxl8_ui_frame_end(pxl8_ui* ui);\n" "void pxl8_ui_input_keydown(pxl8_ui* ui, int key);\n" "void pxl8_ui_input_keyup(pxl8_ui* ui, int key);\n" "void pxl8_ui_input_mousedown(pxl8_ui* ui, int x, int y, int button);\n" "void pxl8_ui_input_mousemove(pxl8_ui* ui, int x, int y);\n" "void pxl8_ui_input_mouseup(pxl8_ui* ui, int x, int y, int button);\n" "void pxl8_ui_input_scroll(pxl8_ui* ui, int x, int y);\n" "void pxl8_ui_input_text(pxl8_ui* ui, const char* text);\n" "bool pxl8_ui_button(pxl8_ui* ui, const char* label);\n" "bool pxl8_ui_checkbox(pxl8_ui* ui, const char* label, bool* state);\n" "bool pxl8_ui_has_mouse_focus(pxl8_ui* ui);\n" "void pxl8_ui_indent(pxl8_ui* ui, int amount);\n" "void pxl8_ui_label(pxl8_ui* ui, const char* text);\n" "void pxl8_ui_layout_row(pxl8_ui* ui, int item_count, const int* widths, int height);\n" "int pxl8_ui_menu(pxl8_ui* ui, pxl8_menu_item* items, int item_count);\n" "void pxl8_ui_panel(pxl8_ui* ui, pxl8_bounds rect, pxl8_frame_theme* theme);\n" "bool pxl8_ui_window_begin(pxl8_ui* ui, const char* title, pxl8_bounds rect, int options);\n" "void pxl8_ui_window_end(pxl8_ui* ui);\n" "void pxl8_ui_window_set_open(pxl8_ui* ui, const char* title, bool open);\n" "pxl8_frame_theme pxl8_ui_theme_default(void);\n"; void pxl8_lua_info(const char* msg) { pxl8_info("%s", msg); } void pxl8_lua_warn(const char* msg) { pxl8_warn("%s", msg); } void pxl8_lua_error(const char* msg) { pxl8_error("%s", msg); } void pxl8_lua_debug(const char* msg) { pxl8_debug("%s", msg); } void pxl8_lua_trace(const char* msg) { pxl8_trace("%s", msg); } static void pxl8_script_set_error(pxl8_script* script, const char* error) { if (!script) return; snprintf(script->last_error, sizeof(script->last_error), "%s", error ? error : "Unknown error"); } const char* pxl8_script_get_last_error(pxl8_script* script) { return script ? script->last_error : "Invalid script context"; } pxl8_script* pxl8_script_create(void) { pxl8_script* script = (pxl8_script*)calloc(1, sizeof(pxl8_script)); if (!script) return NULL; script->L = luaL_newstate(); if (!script->L) { free(script); return NULL; } luaL_openlibs(script->L); lua_getglobal(script->L, "require"); lua_pushstring(script->L, "ffi"); if (lua_pcall(script->L, 1, 1, 0) != 0) { pxl8_script_set_error(script, lua_tostring(script->L, -1)); pxl8_script_destroy(script); return NULL; } lua_getfield(script->L, -1, "cdef"); lua_pushstring(script->L, pxl8_ffi_cdefs); if (lua_pcall(script->L, 1, 0, 0) != 0) { pxl8_script_set_error(script, lua_tostring(script->L, -1)); pxl8_script_destroy(script); return NULL; } lua_pop(script->L, 1); lua_getglobal(script->L, "package"); lua_getfield(script->L, -1, "path"); const char* current_path = lua_tostring(script->L, -1); lua_pop(script->L, 1); lua_pushfstring(script->L, "%s;src/lua/?.lua", current_path); lua_setfield(script->L, -2, "path"); lua_pop(script->L, 1); if (luaL_dofile(script->L, "lib/fennel/fennel.lua") == 0) { lua_setglobal(script->L, "fennel"); lua_getglobal(script->L, "fennel"); lua_getfield(script->L, -1, "install"); if (lua_isfunction(script->L, -1)) { if (lua_pcall(script->L, 0, 0, 0) != 0) { pxl8_warn("Failed to install fennel searcher: %s", lua_tostring(script->L, -1)); lua_pop(script->L, 1); } } else { lua_pop(script->L, 1); } lua_pop(script->L, 1); } script->last_error[0] = '\0'; return script; } void pxl8_script_destroy(pxl8_script* script) { if (!script) return; if (script->L) { lua_close(script->L); } free(script); } void pxl8_script_set_gfx(pxl8_script* script, pxl8_gfx* gfx) { if (!script) return; script->gfx = gfx; if (script->L && gfx) { lua_pushlightuserdata(script->L, gfx); lua_setglobal(script->L, "_pxl8_gfx"); } } void pxl8_script_set_input(pxl8_script* script, pxl8_input_state* input) { if (!script) return; script->input = input; if (script->L && input) { lua_pushlightuserdata(script->L, input); lua_setglobal(script->L, "_pxl8_input"); } } void pxl8_script_set_ui(pxl8_script* script, pxl8_ui* ui) { if (!script) return; script->ui = ui; if (script->L && ui) { lua_pushlightuserdata(script->L, ui); lua_setglobal(script->L, "_pxl8_ui"); } } static pxl8_result pxl8_script_prepare_path(pxl8_script* script, const char* filename, char* out_basename, size_t basename_size) { char filename_copy[PATH_MAX]; strncpy(filename_copy, filename, sizeof(filename_copy) - 1); filename_copy[sizeof(filename_copy) - 1] = '\0'; char* last_slash = strrchr(filename_copy, '/'); if (last_slash) { *last_slash = '\0'; char script_dir[PATH_MAX]; char original_cwd[PATH_MAX]; if (realpath(filename_copy, script_dir) && getcwd(original_cwd, sizeof(original_cwd))) { chdir(script_dir); pxl8_script_set_cart_path(script, script_dir, original_cwd); strncpy(out_basename, last_slash + 1, basename_size - 1); out_basename[basename_size - 1] = '\0'; } else { strncpy(out_basename, filename, basename_size - 1); out_basename[basename_size - 1] = '\0'; } } else { strncpy(out_basename, filename, basename_size - 1); out_basename[basename_size - 1] = '\0'; } return PXL8_OK; } pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename) { if (!script || !script->L || !filename) return PXL8_ERROR_NULL_POINTER; char basename[PATH_MAX]; pxl8_script_prepare_path(script, filename, basename, sizeof(basename)); pxl8_result result = PXL8_OK; if (luaL_dofile(script->L, basename) != 0) { pxl8_script_set_error(script, lua_tostring(script->L, -1)); lua_pop(script->L, 1); result = PXL8_ERROR_SCRIPT_ERROR; } else { script->last_error[0] = '\0'; } return result; } void pxl8_script_set_cart_path(pxl8_script* script, const char* cart_path, const char* original_cwd) { if (!script || !script->L || !cart_path || !original_cwd) return; lua_getglobal(script->L, "package"); lua_getfield(script->L, -1, "path"); const char* current_path = lua_tostring(script->L, -1); char new_path[2048]; snprintf(new_path, sizeof(new_path), "%s/?.lua;%s/?/init.lua;%s/src/?.lua;%s/src/?/init.lua;%s/src/lua/?.lua;%s", cart_path, cart_path, cart_path, cart_path, original_cwd, current_path ? current_path : ""); lua_pushstring(script->L, new_path); lua_setfield(script->L, -3, "path"); lua_pop(script->L, 2); lua_getglobal(script->L, "fennel"); if (!lua_isnil(script->L, -1)) { lua_getfield(script->L, -1, "path"); const char* fennel_path = lua_tostring(script->L, -1); snprintf(new_path, sizeof(new_path), "%s/?.fnl;%s/?/init.fnl;%s/src/?.fnl;%s/src/?/init.fnl;%s", cart_path, cart_path, cart_path, cart_path, fennel_path ? fennel_path : ""); lua_pushstring(script->L, new_path); lua_setfield(script->L, -3, "path"); lua_pop(script->L, 1); } lua_pop(script->L, 1); } pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filename) { if (!script || !script->L || !filename) return PXL8_ERROR_NULL_POINTER; char basename[PATH_MAX]; pxl8_script_prepare_path(script, filename, basename, sizeof(basename)); lua_getglobal(script->L, "fennel"); if (lua_isnil(script->L, -1)) { pxl8_script_set_error(script, "Fennel not loaded"); lua_pop(script->L, 1); return PXL8_ERROR_SCRIPT_ERROR; } lua_getfield(script->L, -1, "dofile"); lua_pushstring(script->L, basename); pxl8_result result = PXL8_OK; if (lua_pcall(script->L, 1, 0, 0) != 0) { pxl8_script_set_error(script, lua_tostring(script->L, -1)); lua_pop(script->L, 1); result = PXL8_ERROR_SCRIPT_ERROR; } else { script->last_error[0] = '\0'; } lua_pop(script->L, 1); return result; } static pxl8_result pxl8_script_eval_internal(pxl8_script* script, const char* code, bool repl_mode) { if (!script || !script->L || !code) return PXL8_ERROR_NULL_POINTER; char transformed_code[PXL8_MAX_REPL_COMMAND_SIZE]; if (repl_mode) { pxl8_script_repl_promote_locals(code, transformed_code, sizeof(transformed_code)); code = transformed_code; } lua_getglobal(script->L, "fennel"); if (lua_isnil(script->L, -1)) { pxl8_script_set_error(script, "Fennel not loaded"); lua_pop(script->L, 1); return PXL8_ERROR_SCRIPT_ERROR; } lua_getfield(script->L, -1, "eval"); lua_pushstring(script->L, code); lua_newtable(script->L); lua_pushstring(script->L, "useMetadata"); lua_pushboolean(script->L, true); lua_settable(script->L, -3); if (lua_pcall(script->L, 2, 1, 0) != 0) { const char* error = lua_tostring(script->L, -1); if (error) { char cleaned_error[2048]; size_t j = 0; for (size_t i = 0; error[i] && j < sizeof(cleaned_error) - 1; i++) { if (error[i] == '\t') { cleaned_error[j++] = ' '; } else { cleaned_error[j++] = error[i]; } } cleaned_error[j] = '\0'; pxl8_script_set_error(script, cleaned_error); } else { pxl8_script_set_error(script, "Unknown error"); } lua_pop(script->L, 2); return PXL8_ERROR_SCRIPT_ERROR; } if (!lua_isnil(script->L, -1)) { lua_pushvalue(script->L, -1); lua_setglobal(script->L, "_"); lua_getglobal(script->L, "tostring"); lua_pushvalue(script->L, -2); if (lua_pcall(script->L, 1, 1, 0) == 0) { const char* result = lua_tostring(script->L, -1); if (result && strlen(result) > 0) { printf("=> %s\n", result); fflush(stdout); } lua_pop(script->L, 1); } } lua_pop(script->L, 2); script->last_error[0] = '\0'; return PXL8_OK; } pxl8_result pxl8_script_eval(pxl8_script* script, const char* code) { return pxl8_script_eval_internal(script, code, false); } pxl8_result pxl8_script_eval_repl(pxl8_script* script, const char* code) { return pxl8_script_eval_internal(script, code, true); } pxl8_result pxl8_script_load_module(pxl8_script* script, const char* module_name) { if (!script || !script->L || !module_name) return PXL8_ERROR_NULL_POINTER; lua_getglobal(script->L, "require"); lua_pushstring(script->L, module_name); if (lua_pcall(script->L, 1, 1, 0) != 0) { pxl8_script_set_error(script, lua_tostring(script->L, -1)); lua_pop(script->L, 1); return PXL8_ERROR_SCRIPT_ERROR; } lua_setglobal(script->L, module_name); script->last_error[0] = '\0'; return PXL8_OK; } pxl8_result pxl8_script_call_function(pxl8_script* script, const char* name) { if (!script || !script->L || !name) return PXL8_ERROR_NULL_POINTER; lua_getglobal(script->L, name); if (!lua_isfunction(script->L, -1)) { lua_pop(script->L, 1); return PXL8_OK; } if (lua_pcall(script->L, 0, 0, 0) != 0) { pxl8_script_set_error(script, lua_tostring(script->L, -1)); lua_pop(script->L, 1); return PXL8_ERROR_SCRIPT_ERROR; } script->last_error[0] = '\0'; return PXL8_OK; } pxl8_result pxl8_script_call_function_f32(pxl8_script* script, const char* name, f32 arg) { if (!script || !script->L || !name) return PXL8_ERROR_NULL_POINTER; lua_getglobal(script->L, name); if (!lua_isfunction(script->L, -1)) { lua_pop(script->L, 1); return PXL8_OK; } lua_pushnumber(script->L, arg); if (lua_pcall(script->L, 1, 0, 0) != 0) { pxl8_script_set_error(script, lua_tostring(script->L, -1)); lua_pop(script->L, 1); return PXL8_ERROR_SCRIPT_ERROR; } script->last_error[0] = '\0'; return PXL8_OK; } static time_t get_file_mod_time(const char* path) { struct stat file_stat; if (stat(path, &file_stat) == 0) { return file_stat.st_mtime; } return 0; } static time_t get_latest_script_mod_time(const char* dir_path) { DIR* dir = opendir(dir_path); if (!dir) return 0; time_t latest = 0; struct dirent* entry; while ((entry = readdir(dir)) != NULL) { if (entry->d_name[0] == '.') continue; size_t len = strlen(entry->d_name); bool is_script = (len > 4 && strcmp(entry->d_name + len - 4, ".fnl") == 0) || (len > 4 && strcmp(entry->d_name + len - 4, ".lua") == 0); if (is_script) { char full_path[512]; snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); time_t mod_time = get_file_mod_time(full_path); if (mod_time > latest) { latest = mod_time; } } } closedir(dir); return latest; } pxl8_result pxl8_script_load_main(pxl8_script* script, const char* path) { if (!script || !path) return PXL8_ERROR_NULL_POINTER; strncpy(script->main_path, path, sizeof(script->main_path) - 1); script->main_path[sizeof(script->main_path) - 1] = '\0'; char* last_slash = strrchr(script->main_path, '/'); if (last_slash) { size_t dir_len = last_slash - script->main_path; strncpy(script->watch_dir, script->main_path, dir_len); script->watch_dir[dir_len] = '\0'; } else { strcpy(script->watch_dir, "."); } script->latest_mod_time = get_latest_script_mod_time(script->watch_dir); const char* ext = strrchr(path, '.'); pxl8_result result = PXL8_ERROR_INVALID_FORMAT; if (ext && strcmp(ext, ".fnl") == 0) { result = pxl8_script_run_fennel_file(script, path); } else if (ext && strcmp(ext, ".lua") == 0) { result = pxl8_script_run_file(script, path); } else { pxl8_script_set_error(script, "Unknown script type (expected .fnl or .lua)"); return PXL8_ERROR_INVALID_FORMAT; } if (result == PXL8_OK) { pxl8_info("Loaded script: %s", path); } else { pxl8_warn("Failed to load script: %s", script->last_error); } return result; } bool pxl8_script_check_reload(pxl8_script* script) { if (!script || script->main_path[0] == '\0') return false; time_t current_mod_time = get_latest_script_mod_time(script->watch_dir); if (current_mod_time > script->latest_mod_time && current_mod_time != 0) { pxl8_info("Script files modified, reloading: %s", script->main_path); script->latest_mod_time = current_mod_time; const char* ext = strrchr(script->main_path, '.'); if (ext && strcmp(ext, ".fnl") == 0) { if (pxl8_script_run_fennel_file(script, script->main_path) == PXL8_OK) { pxl8_script_call_function(script, "init"); return true; } } else if (ext && strcmp(ext, ".lua") == 0) { if (pxl8_script_run_file(script, script->main_path) == PXL8_OK) { pxl8_script_call_function(script, "init"); return true; } } } return false; } #include #include #include #define PXL8_REPL_RING_BUFFER_SIZE 64 struct pxl8_script_repl_command { char buffer[PXL8_MAX_REPL_COMMAND_SIZE]; }; struct pxl8_script_repl { pthread_t thread; pthread_mutex_t mutex; pthread_cond_t cond; pxl8_script_repl_command ring_buffer[PXL8_REPL_RING_BUFFER_SIZE]; u32 head; u32 tail; bool running; bool should_quit; bool waiting_for_eval; char accumulator[PXL8_MAX_REPL_COMMAND_SIZE]; }; static bool pxl8_script_is_incomplete_error(const char* error) { if (!error) return false; return strstr(error, "expected closing delimiter") != NULL || strstr(error, "unexpected end of source") != NULL || strstr(error, "expected whitespace before") != NULL || strstr(error, "unexpected end") != NULL; } static void pxl8_script_repl_completion(const char* buf, linenoiseCompletions* lc) { const char* fennel_keywords[] = { "fn", "let", "var", "set", "global", "local", "if", "when", "do", "while", "for", "each", "lambda", "λ", "partial", "macro", "macros", "require", "include", "import-macros", "values", "select", "table", "length", ".", "..", ":", "->", "->>", "-?>", "-?>>", "doto", "match", "case", "pick-values", "collect", "icollect", "accumulate" }; const char* pxl8_functions[] = { "pxl8.clr", "pxl8.pixel", "pxl8.get_pixel", "pxl8.line", "pxl8.rect", "pxl8.rect_fill", "pxl8.circle", "pxl8.circle_fill", "pxl8.text", "pxl8.get_screen", "pxl8.info", "pxl8.warn", "pxl8.error", "pxl8.debug", "pxl8.trace" }; size_t buf_len = strlen(buf); for (size_t i = 0; i < sizeof(fennel_keywords) / sizeof(fennel_keywords[0]); i++) { if (strncmp(buf, fennel_keywords[i], buf_len) == 0) { linenoiseAddCompletion(lc, fennel_keywords[i]); } } for (size_t i = 0; i < sizeof(pxl8_functions) / sizeof(pxl8_functions[0]); i++) { if (strncmp(buf, pxl8_functions[i], buf_len) == 0) { linenoiseAddCompletion(lc, pxl8_functions[i]); } } } static char* pxl8_script_repl_hints(const char* buf, int* color, int* bold) { if (strncmp(buf, "pxl8.", 5) == 0 && strlen(buf) == 5) { *color = 35; *bold = 0; return "clr|pixel|line|rect|circle|text|get_screen"; } if (strcmp(buf, "(fn") == 0) { *color = 36; *bold = 0; return " [args] body)"; } if (strcmp(buf, "(let") == 0) { *color = 36; *bold = 0; return " [bindings] body)"; } return NULL; } static void* pxl8_script_repl_stdin_thread(void* user_data) { pxl8_script_repl* repl = (pxl8_script_repl*)user_data; char* line; const char* history_file = ".pxl8_history"; linenoiseHistorySetMaxLen(100); linenoiseSetMultiLine(1); linenoiseSetCompletionCallback(pxl8_script_repl_completion); linenoiseSetHintsCallback(pxl8_script_repl_hints); linenoiseHistoryLoad(history_file); while (repl->running) { pthread_mutex_lock(&repl->mutex); bool in_multiline = (repl->accumulator[0] != '\0'); pthread_mutex_unlock(&repl->mutex); const char* prompt = in_multiline ? ".. " : ">> "; line = linenoise(prompt); if (!line) break; if (strlen(line) > 0 || in_multiline) { if (!in_multiline) { linenoiseHistoryAdd(line); linenoiseHistorySave(history_file); } pthread_mutex_lock(&repl->mutex); if (repl->accumulator[0] != '\0') { strncat(repl->accumulator, "\n", PXL8_MAX_REPL_COMMAND_SIZE - strlen(repl->accumulator) - 1); } strncat(repl->accumulator, line, PXL8_MAX_REPL_COMMAND_SIZE - strlen(repl->accumulator) - 1); u32 next_tail = (repl->tail + 1) % PXL8_REPL_RING_BUFFER_SIZE; if (next_tail != repl->head) { strncpy(repl->ring_buffer[repl->tail].buffer, repl->accumulator, PXL8_MAX_REPL_COMMAND_SIZE - 1); repl->ring_buffer[repl->tail].buffer[PXL8_MAX_REPL_COMMAND_SIZE - 1] = '\0'; repl->tail = next_tail; repl->waiting_for_eval = true; while (repl->waiting_for_eval && repl->running) { pthread_cond_wait(&repl->cond, &repl->mutex); } } pthread_mutex_unlock(&repl->mutex); } linenoiseFree(line); } pthread_mutex_lock(&repl->mutex); repl->should_quit = true; pthread_mutex_unlock(&repl->mutex); return NULL; } pxl8_script_repl* pxl8_script_repl_create(void) { pxl8_script_repl* repl = (pxl8_script_repl*)calloc(1, sizeof(pxl8_script_repl)); return repl; } void pxl8_script_repl_destroy(pxl8_script_repl* repl) { if (!repl) return; free(repl); } void pxl8_script_repl_init(pxl8_script_repl* repl) { if (!repl) return; repl->head = 0; repl->tail = 0; repl->running = true; repl->waiting_for_eval = false; repl->accumulator[0] = '\0'; pthread_mutex_init(&repl->mutex, NULL); pthread_cond_init(&repl->cond, NULL); pthread_create(&repl->thread, NULL, pxl8_script_repl_stdin_thread, repl); } void pxl8_script_repl_shutdown(pxl8_script_repl* repl) { if (!repl) return; pthread_mutex_lock(&repl->mutex); repl->running = false; pthread_cond_signal(&repl->cond); pthread_mutex_unlock(&repl->mutex); pthread_cancel(repl->thread); pthread_join(repl->thread, NULL); system("stty sane 2>/dev/null"); pthread_cond_destroy(&repl->cond); pthread_mutex_destroy(&repl->mutex); } pxl8_script_repl_command* pxl8_script_repl_pop_command(pxl8_script_repl* repl) { if (!repl) return NULL; pthread_mutex_lock(&repl->mutex); pxl8_script_repl_command* cmd = NULL; if (repl->head != repl->tail) { cmd = &repl->ring_buffer[repl->head]; repl->head = (repl->head + 1) % PXL8_REPL_RING_BUFFER_SIZE; } pthread_mutex_unlock(&repl->mutex); return cmd; } const char* pxl8_script_repl_command_buffer(pxl8_script_repl_command* cmd) { return cmd ? cmd->buffer : NULL; } bool pxl8_script_repl_should_quit(pxl8_script_repl* repl) { if (!repl) return false; pthread_mutex_lock(&repl->mutex); bool should_quit = repl->should_quit; pthread_mutex_unlock(&repl->mutex); return should_quit; } void pxl8_script_repl_eval_complete(pxl8_script_repl* repl) { if (!repl) return; pthread_mutex_lock(&repl->mutex); repl->waiting_for_eval = false; pthread_cond_signal(&repl->cond); pthread_mutex_unlock(&repl->mutex); } void pxl8_script_repl_clear_accumulator(pxl8_script_repl* repl) { if (!repl) return; pthread_mutex_lock(&repl->mutex); repl->accumulator[0] = '\0'; pthread_mutex_unlock(&repl->mutex); } bool pxl8_script_is_incomplete_input(pxl8_script* script) { if (!script) return false; return pxl8_script_is_incomplete_error(script->last_error); }