module dlib.platform; import dlib.aliases; import dlib.alloc : Reset; import dlib.alloc; import dlib.util; import includes; import std.typecons; import std.stdio; import core.memory; import core.thread.osthread; import core.time; import core.volatile; import core.atomic; const WINDOW_EDGE_BUFFER = 50; enum Input { None, // Keyboard a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, Zero, One, Two, Three, Four, Five, Six, Seven, Eight, Nine, Num0, Num1, Num2, Num3, Num4, Num5, Num6, Num7, Num8, Num9, NumLock, NumSlash, NumStar, NumMinus, NumPlus, NumEnter, NumPeriod, Insert, Delete, Home, End, PageUp, PageDown, PrintScreen, ScrollLock, Pause, Comma, Period, BackSlash, Backspace, ForwardSlash, Minus, Plus, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, Up, Down, Left, Right, LeftCtrl, LeftAlt, LeftShift, LeftSuper, Tab, CapsLock, RightCtrl, RightAlt, RightSuper, RightShift, Enter, Space, Tilde, Esc, Semicolon, Quote, LeftBrace, RightBrace, // Mouse MouseMotion, LeftClick, MiddleClick, RightClick, }; enum Modifier : u32 { None = 0x00, LeftShift = 0x01, RightShift = 0x02, LeftCtrl = 0x04, RightCtrl = 0x08, LeftAlt = 0x10, RightAlt = 0x20, } alias MD = Modifier; struct InputEvent { Input key; Modifier md; bool pressed; i32 x; i32 y; i32 rel_x; i32 rel_y; } struct Inputs { DLList!(InputEvent) list; Arena arena; } Inputs* GetEvents(PlatformWindow* window) { Lock(&window.input_mutex); Inputs* inputs = &window.inputs[window.input_idx]; window.input_idx = (window.input_idx + 1) % 2; ResetInputs(&window.inputs[window.input_idx]); Unlock(&window.input_mutex); return inputs; } Inputs* GetInputs(PlatformWindow* window) { Lock(&window.input_mutex); Inputs* inputs = window.inputs.ptr + window.input_idx; return inputs; } void ReturnInputs(PlatformWindow* window) { Unlock(&window.input_mutex); } enum SysMessageType { None, Quit, LockCursor, UnlockCursor, HideCursor, ShowCursor, } alias SMT = SysMessageType; struct Mut { bool locked; } struct TicketMut { u64 ticket; u64 next_ticket; } Mut CreateMut() { return Mut(locked: false); } bool TryLock(Mut* mut) { return cas!(MemoryOrder.acq, MemoryOrder.raw)(&mut.locked, false, true); } void Unlock(Mut* mut) { atomicStore!(MemoryOrder.rel, bool)(mut.locked, false); } TicketMut CreateTicketMut() { return TicketMut(ticket: 0, next_ticket: 0); } void Lock(TicketMut* mut) { u64 ticket = atomicFetchAdd!(MemoryOrder.acq, u64)(mut.ticket, 1); while (ticket != volatileLoad(&mut.next_ticket)) {} } void Unlock(TicketMut* mut) { atomicFetchAdd!(MemoryOrder.rel, u64)(mut.next_ticket, 1); } const DNode!(SysMessage) g_sys_message; DNode!(SysMessage)* g_NIL_MSG; struct SysMessage { SysMessageType type; } struct MessageQueue { DLList!(SysMessage) list; DLList!(SysMessage) free_list; TicketMut mut; Arena arena; } MessageQueue CreateMessageQueue() { if (g_NIL_MSG == null) { g_NIL_MSG = cast(DNode!(SysMessage)*)&g_sys_message; } MessageQueue queue = { list: { first: g_NIL_MSG, last: g_NIL_MSG, }, free_list: { first: g_NIL_MSG, last: g_NIL_MSG, }, mut: CreateTicketMut(), arena: CreateArena(MB(1)), }; return queue; } pragma(inline) bool Nil(DNode!(SysMessage)* msg) { return msg == null || msg == g_NIL_MSG; } DNode!(SysMessage)* Pop(MessageQueue* queue) { Lock(&queue.mut); DNode!(SysMessage)* node = DLLPop(&queue.list, g_NIL_MSG); Unlock(&queue.mut); return node; } void Push(PlatformWindow* w, SysMessageType msg) { Lock(&w.msg_queue.mut); DNode!(SysMessage)* node = g_NIL_MSG; if (Nil(w.msg_queue.free_list.first)) { node = Alloc!(DNode!(SysMessage))(&w.msg_queue.arena); } else { node = DLLPop(&w.msg_queue.free_list, g_NIL_MSG); } node.value.type = msg; DLLPush(&w.msg_queue.list, node, g_NIL_MSG); Unlock(&w.msg_queue.mut); } void PushFree(PlatformWindow* w, DNode!(SysMessage)* node) { DLLPush(&w.msg_queue.list, node, g_NIL_MSG); } version(linux) { import core.sys.posix.dlfcn; import core.sys.posix.sys.mman; import core.sys.linux.sys.inotify; import core.sys.linux.fcntl; import core.sys.posix.unistd; import core.sys.posix.pthread : PThread = pthread_t, PThreadCond = pthread_cond_t, PThreadMutex = pthread_mutex_t, PThreadMutexInit = pthread_mutex_init, PThreadCondInit = pthread_cond_init, PThreadCreate = pthread_create, PThreadMutexLock = pthread_mutex_lock, PThreadCondWait = pthread_cond_wait, PThreadMutexUnlock = pthread_mutex_unlock, PThreadCondSignal = pthread_cond_signal, PThreadExit = pthread_exit; import core.stdc.string : strlen; struct SysThread { PThread handle; PThreadCond cond; PThreadMutex mut; } alias PThreadProc = extern (C) void* function(void*); SysThread CreateThread(void* proc, void* param) { SysThread thread; assert(!PThreadMutexInit(&thread.mut, null)); assert(!PThreadCondInit(&thread.cond, null)); assert(!PThreadCreate(&thread.handle, null, cast(PThreadProc)proc, param)); return thread; } void Suspend(SysThread* thread) { PThreadMutexLock(&thread.mut); PThreadCondWait(&thread.cond, &thread.mut); PThreadMutexUnlock(&thread.mut); } void Wake(SysThread* thread) { PThreadCondSignal(&thread.cond); } void Kill() { PThreadExit(null); } void ResetInputs(Inputs* inputs) { inputs.list.first = inputs.list.last = null; Reset(&inputs.arena); } void Push(Inputs* inputs, Input input, i32 x, i32 y, bool pressed, Modifier md) { DNode!(InputEvent)* node = Alloc!(DNode!(InputEvent))(&inputs.arena); node.value.key = input; node.value.pressed = pressed; node.value.x = x; node.value.y = y; node.value.md = md; DLLPushFront(&inputs.list, node, null); } void PushMotion(Inputs* inputs, i32 rel_x, i32 rel_y, i32 x, i32 y) { DNode!(InputEvent)* node = Alloc!(DNode!(InputEvent))(&inputs.arena); node.value.key = Input.MouseMotion; node.value.rel_x = rel_x; node.value.rel_y = rel_y; node.value.x = x; node.value.y = y; node.value.pressed = false; DLLPushFront(&inputs.list, node, null); } struct PlatformWindow { Display *display; xcb_connection_t *conn; xcb_screen_t *screen; xcb_window_t window; xcb_atom_t close_event; xcb_atom_t minimize_event; u16 w; u16 h; i32 mouse_prev_x; i32 mouse_prev_y; bool locked_cursor; bool close; Modifier modifier; SysThread thread; MessageQueue msg_queue; u32 input_idx; Inputs[2] inputs; TicketMut input_mutex; }; struct Library { void* ptr; }; struct Function { void* ptr; }; pragma(inline) bool NilErr(PlatformWindow* window, xcb_void_cookie_t* cookie, xcb_generic_error_t* err) { bool result = err == null; pureFree(err); return result; } pragma(inline) void CheckErr(PlatformWindow *window, xcb_void_cookie_t *cookie, xcb_generic_error_t *err, string msg) { assert(err == null, msg); pureFree(err); }; PlatformWindow CreateWindow(string name, u16 width, u16 height) { PlatformWindow window = { w: width, h: height, input_mutex: CreateTicketMut(), msg_queue: CreateMessageQueue(), inputs: [ { arena: CreateArena(MB(1)) }, { arena: CreateArena(MB(1)) }, ], }; assert(width > 0 && height > 0, "CreateWindow error: width and height must be above 0"); window.display = XOpenDisplay(null); assert(window.display != null, "XOpenDisplay failure"); window.conn = XGetXCBConnection(window.display); assert(window.conn != null, "XGetXCBConnection failure"); xcb_void_cookie_t cookie; xcb_generic_error_t *error; xcb_setup_t *setup = xcb_get_setup(window.conn); xcb_screen_iterator_t iter = xcb_setup_roots_iterator(setup); window.screen = iter.data; i32 event_mask = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE | XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_STRUCTURE_NOTIFY; i32[2] val_win = [window.screen.black_pixel, event_mask]; i32 val_mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK; window.window = xcb_generate_id(window.conn); cookie = xcb_create_window_checked( window.conn, XCB_COPY_FROM_PARENT, window.window, window.screen.root, 0, // x pos 0, // y pos window.w, // width window.h, // height 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, window.screen.root_visual, val_mask, val_win.ptr ); error = xcb_request_check(window.conn, cookie); CheckErr(&window, &cookie, error, "xcb_create_window failure"); pureFree(error); cookie = xcb_map_window_checked(window.conn, window.window); error = xcb_request_check(window.conn, cookie); CheckErr(&window, &cookie, error, "xcb_map_window_checked failure"); pureFree(error); cookie = xcb_change_property_checked( window.conn, XCB_PROP_MODE_REPLACE, window.window, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8, cast(u32)name.length, name.ptr ); error = xcb_request_check(window.conn, cookie); CheckErr(&window, &cookie, error, "xcb_change_property_checked failure"); pureFree(error); xcb_intern_atom_cookie_t c_proto = xcb_intern_atom(window.conn, 1, 12, "WM_PROTOCOLS"); xcb_intern_atom_reply_t *r_proto = xcb_intern_atom_reply(window.conn, c_proto, &error); CheckErr(&window, &cookie, error, "xcb_intern_atom WM_PROTOCOLS failure"); pureFree(error); xcb_intern_atom_cookie_t c_close = xcb_intern_atom(window.conn, 0, 16, "WM_DELETE_WINDOW"); xcb_intern_atom_reply_t *r_close = xcb_intern_atom_reply(window.conn, c_close, &error); CheckErr(&window, &cookie, error, "xcb_intern_atom WM_DELETE_WINDOW failure"); pureFree(error); xcb_intern_atom_cookie_t c_minimize = xcb_intern_atom(window.conn, 0, 20, "_NET_WM_STATE_HIDDEN"); xcb_intern_atom_reply_t *r_minimize = xcb_intern_atom_reply(window.conn, c_minimize, &error); CheckErr(&window, &cookie, error, "xcb_intern_atom _NET_WM_STATE_HIDDEN failure"); pureFree(error); cookie = xcb_change_property_checked( window.conn, XCB_PROP_MODE_REPLACE, window.window, r_proto.atom, XCB_ATOM_ATOM, 32, 1, &r_close.atom ); error = xcb_request_check(window.conn, cookie); CheckErr(&window, &cookie, error, "xcb_change_property_checked failure"); pureFree(error); window.close_event = r_close.atom; window.minimize_event = r_minimize.atom; pureFree(r_proto); pureFree(r_close); pureFree(r_minimize); xcb_map_window(window.conn, window.window); i32 stream_result = xcb_flush(window.conn); assert(stream_result > 0, "xcb_flush failure"); xcb_xfixes_query_version(window.conn, 4, 0); return window; }; void StartPlatformThread(PlatformWindow* window) { window.thread = CreateThread(&HandleEvents, window); } bool LockCursor(PlatformWindow* window) { bool result = window.locked_cursor; if (!window.locked_cursor) { u32 counter = 0; for(;;) { xcb_generic_error_t *error; xcb_grab_pointer_cookie_t grab_cookie = xcb_grab_pointer(window.conn, true, window.window, 0, XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, window.window, XCB_NONE, XCB_CURRENT_TIME); xcb_grab_pointer_reply_t* grab_reply = xcb_grab_pointer_reply(window.conn, grab_cookie, &error); scope(exit) { pureFree(error); pureFree(grab_reply); } if (grab_reply.status == XCB_GRAB_STATUS_SUCCESS) { result = true; break; } if (counter > 5) { break; } counter += 1; Thread.sleep(dur!("msecs")(50)); } HideCursor(window); window.locked_cursor = result; } return result; } void UnlockCursor(PlatformWindow* window) { if (window.locked_cursor) { xcb_ungrab_pointer(window.conn, XCB_CURRENT_TIME); ShowCursor(window); window.locked_cursor = false; } } bool HideCursor(PlatformWindow* window) { xcb_void_cookie_t hide_cursor_cookie = xcb_xfixes_hide_cursor_checked(window.conn, window.window); xcb_generic_error_t *error = xcb_request_check(window.conn, hide_cursor_cookie); return NilErr(window, &hide_cursor_cookie, error); } bool ShowCursor(PlatformWindow* window) { xcb_void_cookie_t show_cursor_cookie = xcb_xfixes_show_cursor_checked(window.conn, window.window); xcb_generic_error_t *error = xcb_request_check(window.conn, show_cursor_cookie); return NilErr(window, &show_cursor_cookie, error); } void FlushEvents(PlatformWindow* window) { xcb_generic_event_t* e; do { e = xcb_poll_for_event(window.conn); } while (e); } void HandleEvents(void* window_ptr) { PlatformWindow* w = cast(PlatformWindow*)window_ptr; DNode!(SysMessage)* sys_msg = g_NIL_MSG; xcb_generic_event_t* e; bool ignore_mouse_events = false; for(;;) { if (w.close) { Kill(); } sys_msg = Pop(&w.msg_queue); if (!Nil(sys_msg)) { SysMessage msg = sys_msg.value; switch (msg.type) { case SMT.Quit: { w.close = true; } break; case SMT.LockCursor: { for(u64 i = 0; i < 5; i += 1) { if (LockCursor(w)) { break; } } } break; case SMT.UnlockCursor: { UnlockCursor(w); } break; case SMT.HideCursor: { for (u64 i = 0; i < 5; i += 1) { if (HideCursor(w)) { break; } } } break; case SMT.ShowCursor: { for(u64 i = 0; i < 5; i += 1) { if (ShowCursor(w)) { break; } } } break; default: break; } } e = xcb_poll_for_event(w.conn); if (e) { Inputs* inputs = GetInputs(w); switch (e.response_type & ~0x80) { case XCB_CLIENT_MESSAGE: { xcb_client_message_event_t* msg = cast(xcb_client_message_event_t*)e; if (msg.window != w.window) { break; } if (msg.data.data32[0] == w.close_event) { w.close = true; } } break; case XCB_KEY_RELEASE: case XCB_KEY_PRESS: { xcb_key_press_event_t* keyboard_event = cast(xcb_key_press_event_t*)e; bool pressed = e.response_type == XCB_KEY_PRESS; xcb_keycode_t code = keyboard_event.detail; KeySym key_sym = XkbKeycodeToKeysym(w.display, cast(KeyCode)code, 0, 0); Input input = ConvertInput(key_sym); enum modifiers = [Input.LeftShift, Input.RightShift, Input.LeftCtrl, Input.RightCtrl, Input.LeftAlt, Input.RightAlt]; static foreach(md; modifiers) { if (input == md) { w.modifier = cast(Modifier)(pressed ? (w.modifier | md) : (w.modifier & ~md)); } } if (input != Input.None) { Push(inputs, input, keyboard_event.event_x, keyboard_event.event_y, pressed, w.modifier); } } break; case XCB_BUTTON_PRESS: case XCB_BUTTON_RELEASE: { xcb_button_press_event_t* mouse_event = cast(xcb_button_press_event_t*)e; bool pressed = e.response_type == XCB_BUTTON_PRESS; Input input = Input.None; switch (mouse_event.detail) { case XCB_BUTTON_INDEX_1: input = Input.LeftClick; break; case XCB_BUTTON_INDEX_2: input = Input.MiddleClick; break; case XCB_BUTTON_INDEX_3: input = Input.RightClick; break; default: break; } if (input != Input.None) { Push(inputs, input, mouse_event.event_x, mouse_event.event_y, pressed, w.modifier); } } break; case XCB_MOTION_NOTIFY: { if (ignore_mouse_events) continue; xcb_motion_notify_event_t* move_event = cast(xcb_motion_notify_event_t*)e; i16 x = move_event.event_x; i16 y = move_event.event_y; static bool first = true; if (first) { w.mouse_prev_x = x; w.mouse_prev_y = y; first = false; } if (x > 0 || y > 0) { PushMotion(inputs, w.mouse_prev_x-x, w.mouse_prev_y-y, x, y); } w.mouse_prev_x = x; w.mouse_prev_y = y; if (w.locked_cursor && (x < WINDOW_EDGE_BUFFER || y < WINDOW_EDGE_BUFFER || x > w.w - WINDOW_EDGE_BUFFER || y > w.h - WINDOW_EDGE_BUFFER)) { i16 new_x = cast(i16)(w.w / 2); i16 new_y = cast(i16)(w.h / 2); xcb_warp_pointer(w.conn, w.window, w.window, 0, 0, cast(i16)w.w, cast(i16)w.h, new_x, new_y); w.mouse_prev_x = new_x; w.mouse_prev_y = new_y; ignore_mouse_events = true; } } break; case XCB_CONFIGURE_NOTIFY: { xcb_configure_notify_event_t* config_event = cast(xcb_configure_notify_event_t*)e; if (w.w != config_event.width || w.h != config_event.height) { w.w = config_event.width; w.h = config_event.height; } } break; default: break; } ReturnInputs(w); } } } Library LoadLibrary(string name) { Library lib = { ptr: null, }; lib.ptr = dlopen(name.ptr, RTLD_NOW); return lib; }; Function LoadFunction(Library lib, string name) { Function fn = { ptr: null, }; fn.ptr = dlsym(lib.ptr, name.ptr); return fn; }; Input ConvertInput(u64 x_key) { switch (x_key) { case XK_BackSpace: return Input.Backspace; case XK_Return: return Input.Enter; case XK_Tab: return Input.Tab; case XK_Pause: return Input.Pause; case XK_Caps_Lock: return Input.CapsLock; case XK_Escape: return Input.Esc; case XK_space: return Input.Space; case XK_Prior: return Input.PageUp; case XK_Next: return Input.PageDown; case XK_End: return Input.End; case XK_Home: return Input.Home; case XK_Left: return Input.Left; case XK_Up: return Input.Up; case XK_Right: return Input.Right; case XK_Down: return Input.Down; case XK_Print: return Input.PrintScreen; case XK_Insert: return Input.Insert; case XK_Delete: return Input.Delete; case XK_Meta_L: case XK_Super_L: return Input.LeftSuper; case XK_Meta_R: case XK_Super_R: return Input.RightSuper; case XK_KP_0: return Input.Num0; case XK_KP_1: return Input.Num1; case XK_KP_2: return Input.Num2; case XK_KP_3: return Input.Num3; case XK_KP_4: return Input.Num4; case XK_KP_5: return Input.Num5; case XK_KP_6: return Input.Num6; case XK_KP_7: return Input.Num7; case XK_KP_8: return Input.Num8; case XK_KP_9: return Input.Num9; case XK_multiply: return Input.NumStar; case XK_KP_Subtract: return Input.NumMinus; case XK_KP_Decimal: return Input.NumPeriod; case XK_KP_Divide: return Input.NumSlash; case XK_KP_Add: return Input.NumPlus; case XK_F1: return Input.F1; case XK_F2: return Input.F2; case XK_F3: return Input.F3; case XK_F4: return Input.F4; case XK_F5: return Input.F5; case XK_F6: return Input.F6; case XK_F7: return Input.F7; case XK_F8: return Input.F8; case XK_F9: return Input.F9; case XK_F10: return Input.F10; case XK_F11: return Input.F11; case XK_F12: return Input.F12; case XK_Num_Lock: return Input.NumLock; case XK_Scroll_Lock: return Input.ScrollLock; case XK_Shift_L: return Input.LeftShift; case XK_Shift_R: return Input.RightShift; case XK_Control_L: return Input.LeftCtrl; case XK_Control_R: return Input.RightCtrl; case XK_Alt_L: return Input.LeftAlt; case XK_Alt_R: return Input.RightAlt; case XK_semicolon: return Input.Semicolon; case XK_bracketleft: return Input.LeftBrace; case XK_bracketright: return Input.RightBrace; case XK_plus: return Input.Plus; case XK_comma: return Input.Comma; case XK_minus: return Input.Minus; case XK_backslash: return Input.BackSlash; case XK_slash: return Input.ForwardSlash; case XK_grave: return Input.Tilde; case XK_0: return Input.Zero; case XK_1: return Input.One; case XK_2: return Input.Two; case XK_3: return Input.Three; case XK_4: return Input.Four; case XK_5: return Input.Five; case XK_6: return Input.Six; case XK_7: return Input.Seven; case XK_8: return Input.Eight; case XK_9: return Input.Nine; case XK_a: return Input.a; case XK_A: return Input.A; case XK_b: return Input.b; case XK_B: return Input.B; case XK_c: return Input.c; case XK_C: return Input.C; case XK_d: return Input.d; case XK_D: return Input.D; case XK_e: return Input.e; case XK_E: return Input.E; case XK_f: return Input.f; case XK_F: return Input.F; case XK_g: return Input.g; case XK_G: return Input.G; case XK_h: return Input.h; case XK_H: return Input.H; case XK_i: return Input.i; case XK_I: return Input.I; case XK_j: return Input.j; case XK_J: return Input.J; case XK_k: return Input.k; case XK_K: return Input.K; case XK_l: return Input.l; case XK_L: return Input.L; case XK_m: return Input.m; case XK_M: return Input.M; case XK_n: return Input.n; case XK_N: return Input.N; case XK_o: return Input.o; case XK_O: return Input.O; case XK_p: return Input.p; case XK_P: return Input.P; case XK_q: return Input.q; case XK_Q: return Input.Q; case XK_r: return Input.r; case XK_R: return Input.R; case XK_s: return Input.s; case XK_S: return Input.S; case XK_t: return Input.t; case XK_T: return Input.T; case XK_u: return Input.u; case XK_U: return Input.U; case XK_v: return Input.v; case XK_V: return Input.V; case XK_w: return Input.w; case XK_W: return Input.W; case XK_x: return Input.x; case XK_X: return Input.X; case XK_y: return Input.y; case XK_Y: return Input.Y; case XK_z: return Input.z; case XK_Z: return Input.Z; default: return Input.None; } } void* MemAlloc(u64 size) { return mmap(null, size, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0); } void MemFree(void* ptr, u64 size) { assert(munmap(ptr, size) == 0, "MemFree failure"); } struct Watcher { Arena arena; u8[] buffer; WatcherH handle; WatchH dir_handle; u8[] watched_dir; bool blocking; } alias WatcherH = int; alias WatchH = int; enum WatchType { None, Access = IN_ACCESS, Metadata = IN_ATTRIB, Create = IN_CREATE, Delete = IN_DELETE, Modify = IN_MODIFY, Moved = IN_MOVED_FROM | IN_MOVED_TO, } alias WT = WatchType; Watcher WatchDirectory(string dir, WatchType type, bool blocking = false) { assert(dir.length > 0); Watcher watcher = { arena: CreateArena(MB(4)), buffer: AllocArray!(u8)(MB(1)), blocking: blocking, watched_dir: (cast(u8*)dir.ptr)[0 .. dir.length], }; watcher.handle = inotify_init(); assert(watcher.dir_handle >= 0, "WatchDirectory failure: unable to initialize"); if (!blocking) { fcntl(watcher.handle, F_SETFL, fcntl(watcher.handle, F_GETFL) | O_NONBLOCK); } watcher.dir_handle = inotify_add_watch(watcher.handle, dir.ptr, type); return watcher; } WatchEvent[] ViewChanges(Watcher* watcher) { assert(watcher.handle >= 0 && watcher.dir_handle >= 0, "ViewChanges failure: handles are not valid"); Reset(&watcher.arena); WatchEvent[] events; i64 length = read(watcher.handle, watcher.buffer.ptr, watcher.buffer.length); if (length > 0) { i64 count = 0; i64 i = 0; while(i < length) { inotify_event* event = (cast(inotify_event*)(watcher.buffer.ptr + i)); count += 1; assert(event.wd == watcher.dir_handle); i += inotify_event.sizeof + event.len; } if (count > 0) { struct Moved { u32 cookie; i64 to; i64 from; } Moved[] moved = AllocArray!(Moved)(&watcher.arena, (count/2)+1); i64 m_count = 0; events = AllocArray!(WatchEvent)(&watcher.arena, count); count = 0; i = 0; while (i < length) { inotify_event* event = (cast(inotify_event*)(watcher.buffer.ptr + i)); if (event.len > 0) { u8[] file_name = (cast(u8*)event.name)[0 .. strlen(event.name.ptr)]; if (event.mask & IN_MOVED_FROM || event.mask & IN_MOVED_TO) { bool from = (event.mask & IN_MOVED_FROM) > 0; Moved* m; foreach(j; 0 .. m_count) { if (moved[j].cookie == event.cookie) { m = moved.ptr + j; } } if (m != null) { if (from && m.to >= 0) { events[m.to].names[0] = file_name; if (watcher.watched_dir == file_name) { events[m.to].type &= ~(WET.File | WET.Dir); events[m.to].type |= WET.Self; } } else if (!from && m.from >= 0) { events[m.from].names[1] = file_name; } } else { WatchEvent* ev = events.ptr + count; ev.type = (event.mask & IN_ISDIR) ? WET.Dir : WET.File; ev.type |= WET.Moved; moved[m_count].cookie = event.cookie; if (from) { ev.names[0] = file_name; moved[m_count].from = count; moved[m_count].to = -1; if (watcher.watched_dir == file_name) { ev.type &= ~(WET.File | WET.Dir); ev.type |= WET.Self; } } else { ev.names[1] = file_name; moved[m_count].to = count; moved[m_count].from = -1; } count += 1; m_count += 1; } } else { WatchEvent* ev = events.ptr + count; ev.type = (event.mask & IN_ISDIR) ? WET.Dir : WET.File; ev.names[0] = file_name; if (ev.names[0] == watcher.watched_dir) { ev.type = WET.Self; } SetEventType(ev, event.mask); count += 1; } } else { WatchEvent* ev = events.ptr + count; ev.type = (event.mask & IN_ISDIR) ? WET.Dir : WET.File; SetEventType(ev, event.mask); count += 1; } i += inotify_event.sizeof + event.len; } } } return events; } void SetEventType(WatchEvent* ev, u32 mask) { if (mask & IN_ACCESS) { ev.type |= WET.Accessed; } else if (mask & IN_ATTRIB) { ev.type |= WET.Metadata; } else if (mask & IN_CREATE) { ev.type |= WET.Created; } else if (mask & IN_MODIFY) { ev.type |= WET.Modified; } else if (mask & IN_DELETE) { ev.type |= WET.Deleted; } } unittest { import std.stdio; import std.file : Remove = remove; Watcher fw = WatchDirectory("./", WT.Create | WT.Delete | WT.Moved | WT.Modify | WT.Access | WT.Metadata | WT.Access | WT.Metadata); auto f = File("test_file.txt", "wb"); f.write("test"); f.sync(); f.close(); Remove("./test_file.txt"); WatchEvent[] events = ViewChanges(&fw); assert(events.length == 3); assert(events[0].type == WET.FileCreated); assert(events[0].names[0] == r"test_file.txt"); assert(events[1].type == WET.FileModified); assert(events[1].names[0] == r"test_file.txt"); assert(events[2].type == WET.FileDeleted); assert(events[2].names[0] == r"test_file.txt"); } } enum WatchEventType { None = 0x0000, File = 0x0001, Dir = 0x0002, Self = 0x0004, Created = 0x0008, Modified = 0x0010, Deleted = 0x0020, Moved = 0x0040, Metadata = 0x0080, Accessed = 0x0100, FileCreated = WET.File | WET.Created, FileModified = WET.File | WET.Modified, FileDeleted = WET.File | WET.Deleted, FileMoved = WET.File | WET.Moved, FileMetadata = WET.File | WET.Metadata, FileAccessed = WET.File | WET.Accessed, DirCreated = WET.Dir | WET.Created, DirModified = WET.Dir | WET.Modified, DirDeleted = WET.Dir | WET.Deleted, DirMoved = WET.Dir | WET.Moved, DirMetadata = WET.Dir | WET.Metadata, DirAccessed = WET.Dir | WET.Accessed, SelfCreated = WET.Self | WET.Created, SelfModified = WET.Self | WET.Modified, SelfDeleted = WET.Self | WET.Deleted, SelfMoved = WET.Self | WET.Moved, SelfMetadata = WET.Self | WET.Metadata, SelfAccessed = WET.Self | WET.Accessed, } alias WET = WatchEventType; struct WatchEvent { WatchEventType type; u8[][2] names; } version(Windows) { import core.sys.windows.windows; struct SysThread { HANDLE handle; } alias Win32ThreadProc = extern (C) u32 function(void*); SysThread CreateThread(void* proc, void* param) { SysThread thread; CreateThread(null, 0, cast(Win32ThreadProc)proc, param, 0, null); return thread; } void Suspend(SysThread* thread) { SuspendThread(thread.handle); } void Wake(SysThread* thread) { ResumeThread(thread.handle); } void Kill() { ExitThread(0); } }