1#include <SDL.h>
2#include <string.h>
3#include <stdbool.h>
4#include <ctype.h>
5#include <errno.h>
6#include <sys/types.h>
7#include <sys/stat.h>
8#include "api.h"
9#include "../rencache.h"
10#include "../renwindow.h"
11#ifdef _WIN32
12 #include <direct.h>
13 #include <windows.h>
14 #include <fileapi.h>
15 #include "../utfconv.h"
16
17 // Windows does not define the S_ISREG and S_ISDIR macros in stat.h, so we do.
18 // We have to define _CRT_INTERNAL_NONSTDC_NAMES 1 before #including sys/stat.h
19 // in order for Microsoft's stat.h to define names like S_IFMT, S_IFREG, and S_IFDIR,
20 // rather than just defining _S_IFMT, _S_IFREG, and _S_IFDIR as it normally does.
21 #define _CRT_INTERNAL_NONSTDC_NAMES 1
22 #include <sys/types.h>
23 #include <sys/stat.h>
24 #if !defined(S_ISREG) && defined(S_IFMT) && defined(S_IFREG)
25 #define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
26 #endif
27 #if !defined(S_ISDIR) && defined(S_IFMT) && defined(S_IFDIR)
28 #define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
29 #endif
30#else
31
32#include <dirent.h>
33#include <unistd.h>
34
35#ifdef __linux__
36 #include <sys/vfs.h>
37#endif
38#endif
39
40static const char* button_name(int button) {
41 switch (button) {
42 case SDL_BUTTON_LEFT : return "left";
43 case SDL_BUTTON_MIDDLE : return "middle";
44 case SDL_BUTTON_RIGHT : return "right";
45 case SDL_BUTTON_X1 : return "x";
46 case SDL_BUTTON_X2 : return "y";
47 default : return "?";
48 }
49}
50
51
52static void str_tolower(char *p) {
53 while (*p) {
54 *p = tolower(*p);
55 p++;
56 }
57}
58
59struct HitTestInfo {
60 int title_height;
61 int controls_width;
62 int resize_border;
63};
64typedef struct HitTestInfo HitTestInfo;
65
66static HitTestInfo window_hit_info[1] = {{0, 0, 0}};
67
68#define RESIZE_FROM_TOP 0
69#define RESIZE_FROM_RIGHT 0
70
71static SDL_HitTestResult SDLCALL hit_test(SDL_Window *window, const SDL_Point *pt, void *data) {
72 const HitTestInfo *hit_info = (HitTestInfo *) data;
73 const int resize_border = hit_info->resize_border;
74 const int controls_width = hit_info->controls_width;
75 int w, h;
76
77 SDL_GetWindowSize(window_renderer.window, &w, &h);
78
79 if (pt->y < hit_info->title_height &&
80 #if RESIZE_FROM_TOP
81 pt->y > hit_info->resize_border &&
82 #endif
83 pt->x > resize_border && pt->x < w - controls_width) {
84 return SDL_HITTEST_DRAGGABLE;
85 }
86
87 #define REPORT_RESIZE_HIT(name) { \
88 return SDL_HITTEST_RESIZE_##name; \
89 }
90
91 if (pt->x < resize_border && pt->y < resize_border) {
92 REPORT_RESIZE_HIT(TOPLEFT);
93 #if RESIZE_FROM_TOP
94 } else if (pt->x > resize_border && pt->x < w - controls_width && pt->y < resize_border) {
95 REPORT_RESIZE_HIT(TOP);
96 #endif
97 } else if (pt->x > w - resize_border && pt->y < resize_border) {
98 REPORT_RESIZE_HIT(TOPRIGHT);
99 #if RESIZE_FROM_RIGHT
100 } else if (pt->x > w - resize_border && pt->y > resize_border && pt->y < h - resize_border) {
101 REPORT_RESIZE_HIT(RIGHT);
102 #endif
103 } else if (pt->x > w - resize_border && pt->y > h - resize_border) {
104 REPORT_RESIZE_HIT(BOTTOMRIGHT);
105 } else if (pt->x < w - resize_border && pt->x > resize_border && pt->y > h - resize_border) {
106 REPORT_RESIZE_HIT(BOTTOM);
107 } else if (pt->x < resize_border && pt->y > h - resize_border) {
108 REPORT_RESIZE_HIT(BOTTOMLEFT);
109 } else if (pt->x < resize_border && pt->y < h - resize_border && pt->y > resize_border) {
110 REPORT_RESIZE_HIT(LEFT);
111 }
112
113 return SDL_HITTEST_NORMAL;
114}
115
116static const char *numpad[] = { "end", "down", "pagedown", "left", "", "right", "home", "up", "pageup", "ins", "delete" };
117
118static const char *get_key_name(const SDL_Event *e, char *buf) {
119 SDL_Scancode scancode = e->key.keysym.scancode;
120 /* Is the scancode from the keypad and the number-lock off?
121 ** We assume that SDL_SCANCODE_KP_1 up to SDL_SCANCODE_KP_9 and SDL_SCANCODE_KP_0
122 ** and SDL_SCANCODE_KP_PERIOD are declared in SDL2 in that order. */
123 if (scancode >= SDL_SCANCODE_KP_1 && scancode <= SDL_SCANCODE_KP_1 + 10 &&
124 !(e->key.keysym.mod & KMOD_NUM)) {
125 return numpad[scancode - SDL_SCANCODE_KP_1];
126 } else {
127 /* We need to correctly handle non-standard layouts such as dvorak.
128 Therefore, if a Latin letter(code<128) is pressed in the current layout,
129 then we transmit it as it is. But we also need to support shortcuts in
130 other languages, so for non-Latin characters(code>128) we pass the
131 scancode based name that matches the letter in the QWERTY layout.
132
133 In SDL, the codes of all special buttons such as control, shift, arrows
134 and others, are masked with SDLK_SCANCODE_MASK, which moves them outside
135 the unicode range (>0x10FFFF). Users can remap these buttons, so we need
136 to return the correct name, not scancode based. */
137 if ((e->key.keysym.sym < 128) || (e->key.keysym.sym & SDLK_SCANCODE_MASK))
138 strcpy(buf, SDL_GetKeyName(e->key.keysym.sym));
139 else
140 strcpy(buf, SDL_GetScancodeName(scancode));
141 str_tolower(buf);
142 return buf;
143 }
144}
145
146#ifdef _WIN32
147static char *win32_error(DWORD rc) {
148 LPSTR message;
149 FormatMessage(
150 FORMAT_MESSAGE_ALLOCATE_BUFFER |
151 FORMAT_MESSAGE_FROM_SYSTEM |
152 FORMAT_MESSAGE_IGNORE_INSERTS,
153 NULL,
154 rc,
155 MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
156 (LPTSTR) &message,
157 0,
158 NULL
159 );
160
161 return message;
162}
163
164static void push_win32_error(lua_State *L, DWORD rc) {
165 LPSTR message = win32_error(rc);
166 lua_pushstring(L, message);
167 LocalFree(message);
168}
169#endif
170
171static int f_poll_event(lua_State *L) {
172 char buf[16];
173 int mx, my, w, h;
174 SDL_Event e;
175 SDL_Event event_plus;
176
177top:
178 if ( !SDL_PollEvent(&e) ) {
179 return 0;
180 }
181
182 switch (e.type) {
183 case SDL_QUIT:
184 lua_pushstring(L, "quit");
185 return 1;
186
187 case SDL_WINDOWEVENT:
188 if (e.window.event == SDL_WINDOWEVENT_RESIZED) {
189 ren_resize_window(&window_renderer);
190 lua_pushstring(L, "resized");
191 /* The size below will be in points. */
192 lua_pushinteger(L, e.window.data1);
193 lua_pushinteger(L, e.window.data2);
194 return 3;
195 } else if (e.window.event == SDL_WINDOWEVENT_EXPOSED) {
196 rencache_invalidate();
197 lua_pushstring(L, "exposed");
198 return 1;
199 } else if (e.window.event == SDL_WINDOWEVENT_MINIMIZED) {
200 lua_pushstring(L, "minimized");
201 return 1;
202 } else if (e.window.event == SDL_WINDOWEVENT_MAXIMIZED) {
203 lua_pushstring(L, "maximized");
204 return 1;
205 } else if (e.window.event == SDL_WINDOWEVENT_RESTORED) {
206 lua_pushstring(L, "restored");
207 return 1;
208 } else if (e.window.event == SDL_WINDOWEVENT_LEAVE) {
209 lua_pushstring(L, "mouseleft");
210 return 1;
211 }
212 if (e.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
213 lua_pushstring(L, "focuslost");
214 return 1;
215 }
216 /* on some systems, when alt-tabbing to the window SDL will queue up
217 ** several KEYDOWN events for the `tab` key; we flush all keydown
218 ** events on focus so these are discarded */
219 if (e.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) {
220 SDL_FlushEvent(SDL_KEYDOWN);
221 }
222 goto top;
223
224 case SDL_DROPFILE:
225 SDL_GetMouseState(&mx, &my);
226 lua_pushstring(L, "filedropped");
227 lua_pushstring(L, e.drop.file);
228 lua_pushinteger(L, mx);
229 lua_pushinteger(L, my);
230 SDL_free(e.drop.file);
231 return 4;
232
233 case SDL_KEYDOWN:
234#ifdef __APPLE__
235 /* on macos 11.2.3 with sdl 2.0.14 the keyup handler for cmd+w below
236 ** was not enough. Maybe the quit event started to be triggered from the
237 ** keydown handler? In any case, flushing the quit event here too helped. */
238 if ((e.key.keysym.sym == SDLK_w) && (e.key.keysym.mod & KMOD_GUI)) {
239 SDL_FlushEvent(SDL_QUIT);
240 }
241#endif
242 lua_pushstring(L, "keypressed");
243 lua_pushstring(L, get_key_name(&e, buf));
244 return 2;
245
246 case SDL_KEYUP:
247#ifdef __APPLE__
248 /* on macos command+w will close the current window
249 ** we want to flush this event and let the keymapper
250 ** handle this key combination.
251 ** Thanks to mathewmariani, taken from his lite-macos github repository. */
252 if ((e.key.keysym.sym == SDLK_w) && (e.key.keysym.mod & KMOD_GUI)) {
253 SDL_FlushEvent(SDL_QUIT);
254 }
255#endif
256 lua_pushstring(L, "keyreleased");
257 lua_pushstring(L, get_key_name(&e, buf));
258 return 2;
259
260 case SDL_TEXTINPUT:
261 lua_pushstring(L, "textinput");
262 lua_pushstring(L, e.text.text);
263 return 2;
264
265 case SDL_TEXTEDITING:
266 lua_pushstring(L, "textediting");
267 lua_pushstring(L, e.edit.text);
268 lua_pushinteger(L, e.edit.start);
269 lua_pushinteger(L, e.edit.length);
270 return 4;
271
272#if SDL_VERSION_ATLEAST(2, 0, 22)
273 case SDL_TEXTEDITING_EXT:
274 lua_pushstring(L, "textediting");
275 lua_pushstring(L, e.editExt.text);
276 lua_pushinteger(L, e.editExt.start);
277 lua_pushinteger(L, e.editExt.length);
278 SDL_free(e.editExt.text);
279 return 4;
280#endif
281
282 case SDL_MOUSEBUTTONDOWN:
283 if (e.button.button == 1) { SDL_CaptureMouse(1); }
284 lua_pushstring(L, "mousepressed");
285 lua_pushstring(L, button_name(e.button.button));
286 lua_pushinteger(L, e.button.x);
287 lua_pushinteger(L, e.button.y);
288 lua_pushinteger(L, e.button.clicks);
289 return 5;
290
291 case SDL_MOUSEBUTTONUP:
292 if (e.button.button == 1) { SDL_CaptureMouse(0); }
293 lua_pushstring(L, "mousereleased");
294 lua_pushstring(L, button_name(e.button.button));
295 lua_pushinteger(L, e.button.x);
296 lua_pushinteger(L, e.button.y);
297 return 4;
298
299 case SDL_MOUSEMOTION:
300 SDL_PumpEvents();
301 while (SDL_PeepEvents(&event_plus, 1, SDL_GETEVENT, SDL_MOUSEMOTION, SDL_MOUSEMOTION) > 0) {
302 e.motion.x = event_plus.motion.x;
303 e.motion.y = event_plus.motion.y;
304 e.motion.xrel += event_plus.motion.xrel;
305 e.motion.yrel += event_plus.motion.yrel;
306 }
307 lua_pushstring(L, "mousemoved");
308 lua_pushinteger(L, e.motion.x);
309 lua_pushinteger(L, e.motion.y);
310 lua_pushinteger(L, e.motion.xrel);
311 lua_pushinteger(L, e.motion.yrel);
312 return 5;
313
314 case SDL_MOUSEWHEEL:
315 lua_pushstring(L, "mousewheel");
316#if SDL_VERSION_ATLEAST(2, 0, 18)
317 lua_pushnumber(L, e.wheel.preciseY);
318 // Use -x to keep consistency with vertical scrolling values (e.g. shift+scroll)
319 lua_pushnumber(L, -e.wheel.preciseX);
320#else
321 lua_pushinteger(L, e.wheel.y);
322 lua_pushinteger(L, -e.wheel.x);
323#endif
324 return 3;
325
326 case SDL_FINGERDOWN:
327 SDL_GetWindowSize(window_renderer.window, &w, &h);
328
329 lua_pushstring(L, "touchpressed");
330 lua_pushinteger(L, (lua_Integer)(e.tfinger.x * w));
331 lua_pushinteger(L, (lua_Integer)(e.tfinger.y * h));
332 lua_pushinteger(L, e.tfinger.fingerId);
333 return 4;
334
335 case SDL_FINGERUP:
336 SDL_GetWindowSize(window_renderer.window, &w, &h);
337
338 lua_pushstring(L, "touchreleased");
339 lua_pushinteger(L, (lua_Integer)(e.tfinger.x * w));
340 lua_pushinteger(L, (lua_Integer)(e.tfinger.y * h));
341 lua_pushinteger(L, e.tfinger.fingerId);
342 return 4;
343
344 case SDL_FINGERMOTION:
345 SDL_PumpEvents();
346 while (SDL_PeepEvents(&event_plus, 1, SDL_GETEVENT, SDL_FINGERMOTION, SDL_FINGERMOTION) > 0) {
347 e.tfinger.x = event_plus.tfinger.x;
348 e.tfinger.y = event_plus.tfinger.y;
349 e.tfinger.dx += event_plus.tfinger.dx;
350 e.tfinger.dy += event_plus.tfinger.dy;
351 }
352 SDL_GetWindowSize(window_renderer.window, &w, &h);
353
354 lua_pushstring(L, "touchmoved");
355 lua_pushinteger(L, (lua_Integer)(e.tfinger.x * w));
356 lua_pushinteger(L, (lua_Integer)(e.tfinger.y * h));
357 lua_pushinteger(L, (lua_Integer)(e.tfinger.dx * w));
358 lua_pushinteger(L, (lua_Integer)(e.tfinger.dy * h));
359 lua_pushinteger(L, e.tfinger.fingerId);
360 return 6;
361 case SDL_APP_WILLENTERFOREGROUND:
362 case SDL_APP_DIDENTERFOREGROUND:
363 #ifdef LITE_USE_SDL_RENDERER
364 rencache_invalidate();
365 #else
366 SDL_UpdateWindowSurface(window_renderer.window);
367 #endif
368 lua_pushstring(L, e.type == SDL_APP_WILLENTERFOREGROUND ? "enteringforeground" : "enteredforeground");
369 return 1;
370 case SDL_APP_WILLENTERBACKGROUND:
371 lua_pushstring(L, "enteringbackground");
372 return 1;
373 case SDL_APP_DIDENTERBACKGROUND:
374 lua_pushstring(L, "enteredbackground");
375 return 1;
376
377 default:
378 goto top;
379 }
380
381 return 0;
382}
383
384
385static int f_wait_event(lua_State *L) {
386 int nargs = lua_gettop(L);
387 if (nargs >= 1) {
388 double n = luaL_checknumber(L, 1);
389 lua_pushboolean(L, SDL_WaitEventTimeout(NULL, n * 1000));
390 } else {
391 lua_pushboolean(L, SDL_WaitEvent(NULL));
392 }
393 return 1;
394}
395
396
397static SDL_Cursor* cursor_cache[SDL_SYSTEM_CURSOR_HAND + 1];
398
399static const char *cursor_opts[] = {
400 "arrow",
401 "ibeam",
402 "sizeh",
403 "sizev",
404 "hand",
405 NULL
406};
407
408static const int cursor_enums[] = {
409 SDL_SYSTEM_CURSOR_ARROW,
410 SDL_SYSTEM_CURSOR_IBEAM,
411 SDL_SYSTEM_CURSOR_SIZEWE,
412 SDL_SYSTEM_CURSOR_SIZENS,
413 SDL_SYSTEM_CURSOR_HAND
414};
415
416static int f_set_cursor(lua_State *L) {
417 int opt = luaL_checkoption(L, 1, "arrow", cursor_opts);
418 int n = cursor_enums[opt];
419 SDL_Cursor *cursor = cursor_cache[n];
420 if (!cursor) {
421 cursor = SDL_CreateSystemCursor(n);
422 cursor_cache[n] = cursor;
423 }
424 SDL_SetCursor(cursor);
425 return 0;
426}
427
428
429static int f_set_window_title(lua_State *L) {
430 const char *title = luaL_checkstring(L, 1);
431 SDL_SetWindowTitle(window_renderer.window, title);
432 return 0;
433}
434
435
436static const char *window_opts[] = { "normal", "minimized", "maximized", "fullscreen", 0 };
437enum { WIN_NORMAL, WIN_MINIMIZED, WIN_MAXIMIZED, WIN_FULLSCREEN };
438
439static int f_set_window_mode(lua_State *L) {
440 int n = luaL_checkoption(L, 1, "normal", window_opts);
441 SDL_SetWindowFullscreen(window_renderer.window,
442 n == WIN_FULLSCREEN ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
443 if (n == WIN_NORMAL) { SDL_RestoreWindow(window_renderer.window); }
444 if (n == WIN_MAXIMIZED) { SDL_MaximizeWindow(window_renderer.window); }
445 if (n == WIN_MINIMIZED) { SDL_MinimizeWindow(window_renderer.window); }
446 return 0;
447}
448
449
450static int f_set_window_bordered(lua_State *L) {
451 int bordered = lua_toboolean(L, 1);
452 SDL_SetWindowBordered(window_renderer.window, bordered);
453 return 0;
454}
455
456
457static int f_set_window_hit_test(lua_State *L) {
458 if (lua_gettop(L) == 0) {
459 SDL_SetWindowHitTest(window_renderer.window, NULL, NULL);
460 return 0;
461 }
462 window_hit_info->title_height = luaL_checknumber(L, 1);
463 window_hit_info->controls_width = luaL_checknumber(L, 2);
464 window_hit_info->resize_border = luaL_checknumber(L, 3);
465 SDL_SetWindowHitTest(window_renderer.window, hit_test, window_hit_info);
466 return 0;
467}
468
469
470static int f_get_window_size(lua_State *L) {
471 int x, y, w, h;
472 SDL_GetWindowSize(window_renderer.window, &w, &h);
473 SDL_GetWindowPosition(window_renderer.window, &x, &y);
474 lua_pushinteger(L, w);
475 lua_pushinteger(L, h);
476 lua_pushinteger(L, x);
477 lua_pushinteger(L, y);
478 return 4;
479}
480
481
482static int f_set_window_size(lua_State *L) {
483 double w = luaL_checknumber(L, 1);
484 double h = luaL_checknumber(L, 2);
485 double x = luaL_checknumber(L, 3);
486 double y = luaL_checknumber(L, 4);
487 SDL_SetWindowSize(window_renderer.window, w, h);
488 SDL_SetWindowPosition(window_renderer.window, x, y);
489 ren_resize_window(&window_renderer);
490 return 0;
491}
492
493
494static int f_window_has_focus(lua_State *L) {
495 unsigned flags = SDL_GetWindowFlags(window_renderer.window);
496 lua_pushboolean(L, flags & SDL_WINDOW_INPUT_FOCUS);
497 return 1;
498}
499
500
501static int f_get_window_mode(lua_State *L) {
502 unsigned flags = SDL_GetWindowFlags(window_renderer.window);
503 if (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) {
504 lua_pushstring(L, "fullscreen");
505 } else if (flags & SDL_WINDOW_MINIMIZED) {
506 lua_pushstring(L, "minimized");
507 } else if (flags & SDL_WINDOW_MAXIMIZED) {
508 lua_pushstring(L, "maximized");
509 } else {
510 lua_pushstring(L, "normal");
511 }
512 return 1;
513}
514
515static int f_set_text_input_rect(lua_State *L) {
516 SDL_Rect rect;
517 rect.x = luaL_checknumber(L, 1);
518 rect.y = luaL_checknumber(L, 2);
519 rect.w = luaL_checknumber(L, 3);
520 rect.h = luaL_checknumber(L, 4);
521 SDL_SetTextInputRect(&rect);
522 return 0;
523}
524
525static int f_clear_ime(lua_State *L) {
526#if SDL_VERSION_ATLEAST(2, 0, 22)
527 SDL_ClearComposition();
528#endif
529 return 0;
530}
531
532
533static int f_raise_window(lua_State *L) {
534 /*
535 SDL_RaiseWindow should be enough but on some window managers like the
536 one used on Gnome the window needs to first have input focus in order
537 to allow the window to be focused. Also on wayland the raise window event
538 may not always be obeyed.
539 */
540 SDL_SetWindowInputFocus(window_renderer.window);
541 SDL_RaiseWindow(window_renderer.window);
542 return 0;
543}
544
545
546static int f_show_fatal_error(lua_State *L) {
547 const char *title = luaL_checkstring(L, 1);
548 const char *msg = luaL_checkstring(L, 2);
549
550#ifdef _WIN32
551 MessageBox(0, msg, title, MB_OK | MB_ICONERROR);
552#else
553 SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, msg, NULL);
554#endif
555 return 0;
556}
557
558
559// removes an empty directory
560static int f_rmdir(lua_State *L) {
561 const char *path = luaL_checkstring(L, 1);
562
563#ifdef _WIN32
564 LPWSTR wpath = utfconv_utf8towc(path);
565 int deleted = RemoveDirectoryW(wpath);
566 free(wpath);
567 if (deleted > 0) {
568 lua_pushboolean(L, 1);
569 } else {
570 lua_pushboolean(L, 0);
571 push_win32_error(L, GetLastError());
572 return 2;
573 }
574#else
575 int deleted = remove(path);
576 if(deleted < 0) {
577 lua_pushboolean(L, 0);
578 lua_pushstring(L, strerror(errno));
579
580 return 2;
581 } else {
582 lua_pushboolean(L, 1);
583 }
584#endif
585
586 return 1;
587}
588
589
590static int f_chdir(lua_State *L) {
591 const char *path = luaL_checkstring(L, 1);
592#ifdef _WIN32
593 LPWSTR wpath = utfconv_utf8towc(path);
594 if (wpath == NULL) { return luaL_error(L, UTFCONV_ERROR_INVALID_CONVERSION ); }
595 int err = _wchdir(wpath);
596 free(wpath);
597#else
598 int err = chdir(path);
599#endif
600 if (err) { luaL_error(L, "chdir() failed: %s", strerror(errno)); }
601 return 0;
602}
603
604
605static int f_list_dir(lua_State *L) {
606 const char *path = luaL_checkstring(L, 1);
607
608#ifdef _WIN32
609 lua_settop(L, 1);
610 if (path[0] == 0 || strchr("\\/", path[strlen(path) - 1]) != NULL)
611 lua_pushstring(L, "*");
612 else
613 lua_pushstring(L, "/*");
614
615 lua_concat(L, 2);
616 path = lua_tostring(L, -1);
617
618 LPWSTR wpath = utfconv_utf8towc(path);
619 if (wpath == NULL) {
620 lua_pushnil(L);
621 lua_pushstring(L, UTFCONV_ERROR_INVALID_CONVERSION);
622 return 2;
623 }
624
625 WIN32_FIND_DATAW fd;
626 HANDLE find_handle = FindFirstFileExW(wpath, FindExInfoBasic, &fd, FindExSearchNameMatch, NULL, 0);
627 free(wpath);
628 if (find_handle == INVALID_HANDLE_VALUE) {
629 lua_pushnil(L);
630 push_win32_error(L, GetLastError());
631 return 2;
632 }
633
634 char mbpath[MAX_PATH * 4]; // utf-8 spans 4 bytes at most
635 int len, i = 1;
636 lua_newtable(L);
637
638 do
639 {
640 if (wcscmp(fd.cFileName, L".") == 0) { continue; }
641 if (wcscmp(fd.cFileName, L"..") == 0) { continue; }
642
643 len = WideCharToMultiByte(CP_UTF8, 0, fd.cFileName, -1, mbpath, MAX_PATH * 4, NULL, NULL);
644 if (len == 0) { break; }
645 lua_pushlstring(L, mbpath, len - 1); // len includes \0
646 lua_rawseti(L, -2, i++);
647 } while (FindNextFileW(find_handle, &fd));
648
649 if (GetLastError() != ERROR_NO_MORE_FILES) {
650 lua_pushnil(L);
651 push_win32_error(L, GetLastError());
652 FindClose(find_handle);
653 return 2;
654 }
655
656 FindClose(find_handle);
657 return 1;
658#else
659 DIR *dir = opendir(path);
660 if (!dir) {
661 lua_pushnil(L);
662 lua_pushstring(L, strerror(errno));
663 return 2;
664 }
665
666 lua_newtable(L);
667 int i = 1;
668 struct dirent *entry;
669 while ( (entry = readdir(dir)) ) {
670 if (strcmp(entry->d_name, "." ) == 0) { continue; }
671 if (strcmp(entry->d_name, "..") == 0) { continue; }
672 lua_pushstring(L, entry->d_name);
673 lua_rawseti(L, -2, i);
674 i++;
675 }
676
677 closedir(dir);
678 return 1;
679#endif
680}
681
682
683#ifdef _WIN32
684 #define realpath(x, y) _wfullpath(y, x, MAX_PATH)
685#endif
686
687static int f_absolute_path(lua_State *L) {
688 const char *path = luaL_checkstring(L, 1);
689#ifdef _WIN32
690 LPWSTR wpath = utfconv_utf8towc(path);
691 if (!wpath) { return 0; }
692
693 LPWSTR wfullpath = realpath(wpath, NULL);
694 free(wpath);
695 if (!wfullpath) { return 0; }
696
697 char *res = utfconv_wctoutf8(wfullpath);
698 free(wfullpath);
699#else
700 char *res = realpath(path, NULL);
701#endif
702 if (!res) { return 0; }
703 lua_pushstring(L, res);
704 free(res);
705 return 1;
706}
707
708
709static int f_get_file_info(lua_State *L) {
710 const char *path = luaL_checkstring(L, 1);
711
712#ifdef _WIN32
713 struct _stat s;
714 LPWSTR wpath = utfconv_utf8towc(path);
715 if (wpath == NULL) {
716 lua_pushnil(L);
717 lua_pushstring(L, UTFCONV_ERROR_INVALID_CONVERSION);
718 return 2;
719 }
720 int err = _wstat(wpath, &s);
721 free(wpath);
722#else
723 struct stat s;
724 int err = stat(path, &s);
725#endif
726 if (err < 0) {
727 lua_pushnil(L);
728 lua_pushstring(L, strerror(errno));
729 return 2;
730 }
731
732 lua_newtable(L);
733 lua_pushinteger(L, s.st_mtime);
734 lua_setfield(L, -2, "modified");
735
736 lua_pushinteger(L, s.st_size);
737 lua_setfield(L, -2, "size");
738
739 if (S_ISREG(s.st_mode)) {
740 lua_pushstring(L, "file");
741 } else if (S_ISDIR(s.st_mode)) {
742 lua_pushstring(L, "dir");
743 } else {
744 lua_pushnil(L);
745 }
746 lua_setfield(L, -2, "type");
747
748#if __linux__
749 if (S_ISDIR(s.st_mode)) {
750 if (lstat(path, &s) == 0) {
751 lua_pushboolean(L, S_ISLNK(s.st_mode));
752 lua_setfield(L, -2, "symlink");
753 }
754 }
755#endif
756 return 1;
757}
758
759#if __linux__
760// https://man7.org/linux/man-pages/man2/statfs.2.html
761
762struct f_type_names {
763 uint32_t magic;
764 const char *name;
765};
766
767static struct f_type_names fs_names[] = {
768 { 0xef53, "ext2/ext3" },
769 { 0x6969, "nfs" },
770 { 0x65735546, "fuse" },
771 { 0x517b, "smb" },
772 { 0xfe534d42, "smb2" },
773 { 0x52654973, "reiserfs" },
774 { 0x01021994, "tmpfs" },
775 { 0x858458f6, "ramfs" },
776 { 0x5346544e, "ntfs" },
777 { 0x0, NULL },
778};
779
780#endif
781
782static int f_get_fs_type(lua_State *L) {
783 #if __linux__
784 const char *path = luaL_checkstring(L, 1);
785 struct statfs buf;
786 int status = statfs(path, &buf);
787 if (status != 0) {
788 return luaL_error(L, "error calling statfs on %s", path);
789 }
790 for (int i = 0; fs_names[i].magic; i++) {
791 if (fs_names[i].magic == buf.f_type) {
792 lua_pushstring(L, fs_names[i].name);
793 return 1;
794 }
795 }
796 #endif
797 lua_pushstring(L, "unknown");
798 return 1;
799}
800
801
802static int f_mkdir(lua_State *L) {
803 const char *path = luaL_checkstring(L, 1);
804
805#ifdef _WIN32
806 LPWSTR wpath = utfconv_utf8towc(path);
807 if (wpath == NULL) {
808 lua_pushboolean(L, 0);
809 lua_pushstring(L, UTFCONV_ERROR_INVALID_CONVERSION);
810 return 2;
811 }
812
813 int err = _wmkdir(wpath);
814 free(wpath);
815#else
816 int err = mkdir(path, S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH);
817#endif
818 if (err < 0) {
819 lua_pushboolean(L, 0);
820 lua_pushstring(L, strerror(errno));
821 return 2;
822 }
823
824 lua_pushboolean(L, 1);
825 return 1;
826}
827
828
829static int f_get_clipboard(lua_State *L) {
830 char *text = SDL_GetClipboardText();
831 if (!text) { return 0; }
832 lua_pushstring(L, text);
833 SDL_free(text);
834 return 1;
835}
836
837
838static int f_set_clipboard(lua_State *L) {
839 const char *text = luaL_checkstring(L, 1);
840 SDL_SetClipboardText(text);
841 return 0;
842}
843
844
845static int f_get_process_id(lua_State *L) {
846#ifdef _WIN32
847 lua_pushinteger(L, GetCurrentProcessId());
848#else
849 lua_pushinteger(L, getpid());
850#endif
851 return 1;
852}
853
854
855static int f_get_time(lua_State *L) {
856 double n = SDL_GetPerformanceCounter() / (double) SDL_GetPerformanceFrequency();
857 lua_pushnumber(L, n);
858 return 1;
859}
860
861
862static int f_sleep(lua_State *L) {
863 double n = luaL_checknumber(L, 1);
864 SDL_Delay(n * 1000);
865 return 0;
866}
867
868
869static int f_exec(lua_State *L) {
870 size_t len;
871 const char *cmd = luaL_checklstring(L, 1, &len);
872 char *buf = malloc(len + 32);
873 if (!buf) { luaL_error(L, "buffer allocation failed"); }
874#if _WIN32
875 sprintf(buf, "cmd /c \"%s\"", cmd);
876 WinExec(buf, SW_HIDE);
877#else
878 sprintf(buf, "%s &", cmd);
879 int res = system(buf);
880 (void) res;
881#endif
882 free(buf);
883 return 0;
884}
885
886static int f_fuzzy_match(lua_State *L) {
887 size_t strLen, ptnLen;
888 const char *str = luaL_checklstring(L, 1, &strLen);
889 const char *ptn = luaL_checklstring(L, 2, &ptnLen);
890 // If true match things *backwards*. This allows for better matching on filenames than the above
891 // function. For example, in the lite project, opening "renderer" has lib/font_render/build.sh
892 // as the first result, rather than src/renderer.c. Clearly that's wrong.
893 bool files = lua_gettop(L) > 2 && lua_isboolean(L,3) && lua_toboolean(L, 3);
894 int score = 0, run = 0, increment = files ? -1 : 1;
895 const char* strTarget = files ? str + strLen - 1 : str;
896 const char* ptnTarget = files ? ptn + ptnLen - 1 : ptn;
897 while (strTarget >= str && ptnTarget >= ptn && *strTarget && *ptnTarget) {
898 while (strTarget >= str && *strTarget == ' ') { strTarget += increment; }
899 while (ptnTarget >= ptn && *ptnTarget == ' ') { ptnTarget += increment; }
900 if (tolower(*strTarget) == tolower(*ptnTarget)) {
901 score += run * 10 - (*strTarget != *ptnTarget);
902 run++;
903 ptnTarget += increment;
904 } else {
905 score -= 10;
906 run = 0;
907 }
908 strTarget += increment;
909 }
910 if (ptnTarget >= ptn && *ptnTarget) { return 0; }
911 lua_pushinteger(L, score - (int)strLen * 10);
912 return 1;
913}
914
915static int f_set_window_opacity(lua_State *L) {
916 double n = luaL_checknumber(L, 1);
917 int r = SDL_SetWindowOpacity(window_renderer.window, n);
918 lua_pushboolean(L, r > -1);
919 return 1;
920}
921
922typedef void (*fptr)(void);
923
924typedef struct lua_function_node {
925 const char *symbol;
926 fptr address;
927} lua_function_node;
928
929#define P(FUNC) { "lua_" #FUNC, (fptr)(lua_##FUNC) }
930#define U(FUNC) { "luaL_" #FUNC, (fptr)(luaL_##FUNC) }
931#define S(FUNC) { #FUNC, (fptr)(FUNC) }
932static void* api_require(const char* symbol) {
933 static const lua_function_node nodes[] = {
934 #if LUA_VERSION_NUM == 501 || LUA_VERSION_NUM == 502 || LUA_VERSION_NUM == 503 || LUA_VERSION_NUM == 504
935 U(addlstring), U(addstring), U(addvalue), U(argerror), U(buffinit),
936 U(callmeta), U(checkany), U(checkinteger), U(checklstring),
937 U(checknumber), U(checkoption), U(checkstack), U(checktype),
938 U(checkudata), U(error), U(getmetafield), U(gsub), U(loadstring),
939 U(newmetatable), U(newstate), U(openlibs), U(optinteger), U(optlstring),
940 U(optnumber), U(pushresult), U(ref), U(unref), U(where), P(atpanic),
941 P(checkstack), P(close), P(concat), P(createtable), P(dump), P(error),
942 P(gc), P(getallocf), P(getfield), P(gethook), P(gethookcount),
943 P(gethookmask), P(getinfo), P(getlocal), P(getmetatable), P(getstack),
944 P(gettable), P(gettop), P(getupvalue), P(iscfunction), P(isnumber),
945 P(isstring), P(isuserdata), P(load), P(newstate), P(newthread), P(next),
946 P(pushboolean), P(pushcclosure), P(pushfstring), P(pushinteger),
947 P(pushlightuserdata), P(pushlstring), P(pushnil), P(pushnumber),
948 P(pushstring), P(pushthread), P(pushvalue), P(pushvfstring), P(rawequal),
949 P(rawget), P(rawgeti), P(rawset), P(rawseti), P(resume), P(setallocf),
950 P(setfield), P(sethook), P(setlocal), P(setmetatable), P(settable),
951 P(settop), P(setupvalue), P(status), P(toboolean), P(tocfunction),
952 P(tolstring), P(topointer), P(tothread), P(touserdata), P(type),
953 P(typename), P(xmove), S(luaopen_base), S(luaopen_debug), S(luaopen_io),
954 S(luaopen_math), S(luaopen_os), S(luaopen_package), S(luaopen_string),
955 S(luaopen_table), S(api_load_libs),
956 #endif
957 #if LUA_VERSION_NUM == 502 || LUA_VERSION_NUM == 503 || LUA_VERSION_NUM == 504
958 U(buffinitsize), U(checkversion_), U(execresult), U(fileresult),
959 U(getsubtable), U(len), U(loadbufferx), U(loadfilex), U(prepbuffsize),
960 U(pushresultsize), U(requiref), U(setfuncs), U(setmetatable),
961 U(testudata), U(tolstring), U(traceback), P(absindex), P(arith),
962 P(callk), P(compare), P(copy), P(getglobal), P(len), P(pcallk),
963 P(rawgetp), P(rawlen), P(rawsetp), P(setglobal), P(tointegerx),
964 P(tonumberx), P(upvalueid), P(upvaluejoin), P(version), P(yieldk),
965 S(luaopen_coroutine),
966 #endif
967 #if LUA_VERSION_NUM == 501 || LUA_VERSION_NUM == 502 || LUA_VERSION_NUM == 503
968 P(newuserdata),
969 #endif
970 #if LUA_VERSION_NUM == 503 || LUA_VERSION_NUM == 504
971 P(geti), P(isinteger), P(isyieldable), P(rotate), P(seti),
972 P(stringtonumber), S(luaopen_utf8),
973 #endif
974 #if LUA_VERSION_NUM == 502 || LUA_VERSION_NUM == 503
975 P(getuservalue), P(setuservalue), S(luaopen_bit32),
976 #endif
977 #if LUA_VERSION_NUM == 501 || LUA_VERSION_NUM == 502
978 P(insert), P(remove), P(replace),
979 #endif
980 #if LUA_VERSION_NUM == 504
981 U(addgsub), U(typeerror), P(closeslot), P(getiuservalue),
982 P(newuserdatauv), P(resetthread), P(setcstacklimit), P(setiuservalue),
983 P(setwarnf), P(toclose), P(warning),
984 #endif
985 #if LUA_VERSION_NUM == 502
986 U(checkunsigned), U(optunsigned), P(getctx), P(pushunsigned),
987 P(tounsignedx),
988 #endif
989 #if LUA_VERSION_NUM == 501
990 U(findtable), U(loadbuffer), U(loadfile), U(openlib), U(prepbuffer),
991 U(register), U(typerror), P(call), P(cpcall), P(equal), P(getfenv),
992 P(lessthan), P(objlen), P(pcall), P(setfenv), P(setlevel), P(tointeger),
993 P(tonumber), P(yield),
994 #endif
995 };
996 for (size_t i = 0; i < sizeof(nodes) / sizeof(lua_function_node); ++i) {
997 if (strcmp(nodes[i].symbol, symbol) == 0)
998 return *(void**)(&nodes[i].address);
999 }
1000 return NULL;
1001}
1002
1003static int f_library_gc(lua_State *L) {
1004 lua_getfield(L, 1, "handle");
1005 void* handle = lua_touserdata(L, -1);
1006 SDL_UnloadObject(handle);
1007
1008 return 0;
1009}
1010
1011static int f_load_native_plugin(lua_State *L) {
1012 char entrypoint_name[512]; entrypoint_name[sizeof(entrypoint_name) - 1] = '\0';
1013 int result;
1014
1015 const char *name = luaL_checkstring(L, 1);
1016 const char *path = luaL_checkstring(L, 2);
1017 void *library = SDL_LoadObject(path);
1018 if (!library)
1019 return (lua_pushstring(L, SDL_GetError()), lua_error(L));
1020
1021 lua_getglobal(L, "package");
1022 lua_getfield(L, -1, "native_plugins");
1023 lua_newtable(L);
1024 lua_pushlightuserdata(L, library);
1025 lua_setfield(L, -2, "handle");
1026 luaL_setmetatable(L, API_TYPE_NATIVE_PLUGIN);
1027 lua_setfield(L, -2, name);
1028 lua_pop(L, 2);
1029
1030 const char *basename = strrchr(name, '.');
1031 basename = !basename ? name : basename + 1;
1032 snprintf(entrypoint_name, sizeof(entrypoint_name), "luaopen_lite_xl_%s", basename);
1033 int (*ext_entrypoint) (lua_State *L, void* (*)(const char*));
1034 *(void**)(&ext_entrypoint) = SDL_LoadFunction(library, entrypoint_name);
1035 if (!ext_entrypoint) {
1036 snprintf(entrypoint_name, sizeof(entrypoint_name), "luaopen_%s", basename);
1037 int (*entrypoint)(lua_State *L);
1038 *(void**)(&entrypoint) = SDL_LoadFunction(library, entrypoint_name);
1039 if (!entrypoint)
1040 return luaL_error(L, "Unable to load %s: Can't find %s(lua_State *L, void *XL)", name, entrypoint_name);
1041 result = entrypoint(L);
1042 } else {
1043 result = ext_entrypoint(L, api_require);
1044 }
1045
1046 if (!result)
1047 return luaL_error(L, "Unable to load %s: entrypoint must return a value", name);
1048
1049 return result;
1050}
1051
1052#ifdef _WIN32
1053#define PATHSEP '\\'
1054#else
1055#define PATHSEP '/'
1056#endif
1057
1058/* Special purpose filepath compare function. Corresponds to the
1059 order used in the TreeView view of the project's files. Returns true iff
1060 path1 < path2 in the TreeView order. */
1061static int f_path_compare(lua_State *L) {
1062 size_t len1, len2;
1063 const char *path1 = luaL_checklstring(L, 1, &len1);
1064 const char *type1_s = luaL_checkstring(L, 2);
1065 const char *path2 = luaL_checklstring(L, 3, &len2);
1066 const char *type2_s = luaL_checkstring(L, 4);
1067 int type1 = strcmp(type1_s, "dir") != 0;
1068 int type2 = strcmp(type2_s, "dir") != 0;
1069 /* Find the index of the common part of the path. */
1070 size_t offset = 0, i, j;
1071 for (i = 0; i < len1 && i < len2; i++) {
1072 if (path1[i] != path2[i]) break;
1073 if (isdigit(path1[i])) break;
1074 if (path1[i] == PATHSEP) {
1075 offset = i + 1;
1076 }
1077 }
1078 /* If a path separator is present in the name after the common part we consider
1079 the entry like a directory. */
1080 if (strchr(path1 + offset, PATHSEP)) {
1081 type1 = 0;
1082 }
1083 if (strchr(path2 + offset, PATHSEP)) {
1084 type2 = 0;
1085 }
1086 /* If types are different "dir" types comes before "file" types. */
1087 if (type1 != type2) {
1088 lua_pushboolean(L, type1 < type2);
1089 return 1;
1090 }
1091 /* If types are the same compare the files' path alphabetically. */
1092 int cfr = -1;
1093 bool same_len = len1 == len2;
1094 for (i = offset, j = offset; i <= len1 && j <= len2; i++, j++) {
1095 if (path1[i] == 0 || path2[j] == 0) {
1096 if (cfr < 0) cfr = 0; // The strings are equal
1097 if (!same_len) {
1098 cfr = (path1[i] == 0);
1099 }
1100 } else if (isdigit(path1[i]) && isdigit(path2[j])) {
1101 size_t ii = 0, ij = 0;
1102 while (isdigit(path1[i+ii])) { ii++; }
1103 while (isdigit(path2[j+ij])) { ij++; }
1104
1105 size_t di = 0, dj = 0;
1106 for (size_t ai = 0; ai < ii; ++ai) {
1107 di = di * 10 + (path1[i+ai] - '0');
1108 }
1109 for (size_t aj = 0; aj < ij; ++aj) {
1110 dj = dj * 10 + (path2[j+aj] - '0');
1111 }
1112
1113 if (di == dj) {
1114 continue;
1115 }
1116 cfr = (di < dj);
1117 } else if (path1[i] == path2[j]) {
1118 continue;
1119 } else if (path1[i] == PATHSEP || path2[j] == PATHSEP) {
1120 /* For comparison we treat PATHSEP as if it was the string terminator. */
1121 cfr = (path1[i] == PATHSEP);
1122 } else {
1123 char a = path1[i], b = path2[j];
1124 if (a >= 'A' && a <= 'Z') a += 32;
1125 if (b >= 'A' && b <= 'Z') b += 32;
1126 if (a == b) {
1127 /* If the strings have the same length, we need
1128 to keep the first case sensitive difference. */
1129 if (same_len && cfr < 0) {
1130 /* Give priority to lower-case characters */
1131 cfr = (path1[i] > path2[j]);
1132 }
1133 continue;
1134 }
1135 cfr = (a < b);
1136 }
1137 break;
1138 }
1139 lua_pushboolean(L, cfr);
1140 return 1;
1141}
1142
1143
1144static int f_text_input(lua_State* L) {
1145 if (lua_toboolean(L, 1))
1146 SDL_StartTextInput();
1147 else
1148 SDL_StopTextInput();
1149 return 0;
1150}
1151
1152
1153static const luaL_Reg lib[] = {
1154 { "poll_event", f_poll_event },
1155 { "wait_event", f_wait_event },
1156 { "set_cursor", f_set_cursor },
1157 { "set_window_title", f_set_window_title },
1158 { "set_window_mode", f_set_window_mode },
1159 { "get_window_mode", f_get_window_mode },
1160 { "set_window_bordered", f_set_window_bordered },
1161 { "set_window_hit_test", f_set_window_hit_test },
1162 { "get_window_size", f_get_window_size },
1163 { "set_window_size", f_set_window_size },
1164 { "set_text_input_rect", f_set_text_input_rect },
1165 { "clear_ime", f_clear_ime },
1166 { "window_has_focus", f_window_has_focus },
1167 { "raise_window", f_raise_window },
1168 { "show_fatal_error", f_show_fatal_error },
1169 { "rmdir", f_rmdir },
1170 { "chdir", f_chdir },
1171 { "mkdir", f_mkdir },
1172 { "list_dir", f_list_dir },
1173 { "absolute_path", f_absolute_path },
1174 { "get_file_info", f_get_file_info },
1175 { "get_clipboard", f_get_clipboard },
1176 { "set_clipboard", f_set_clipboard },
1177 { "get_process_id", f_get_process_id },
1178 { "get_time", f_get_time },
1179 { "sleep", f_sleep },
1180 { "exec", f_exec },
1181 { "fuzzy_match", f_fuzzy_match },
1182 { "set_window_opacity", f_set_window_opacity },
1183 { "load_native_plugin", f_load_native_plugin },
1184 { "path_compare", f_path_compare },
1185 { "get_fs_type", f_get_fs_type },
1186 { "text_input", f_text_input },
1187 { NULL, NULL }
1188};
1189
1190
1191int luaopen_system(lua_State *L) {
1192 luaL_newmetatable(L, API_TYPE_NATIVE_PLUGIN);
1193 lua_pushcfunction(L, f_library_gc);
1194 lua_setfield(L, -2, "__gc");
1195 luaL_newlib(L, lib);
1196 return 1;
1197}
1198