926 lines
21 KiB
D
926 lines
21 KiB
D
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;
|
|
|
|
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,
|
|
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,
|
|
};
|
|
|
|
alias KBI = Input;
|
|
|
|
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.stdc.string : strlen;
|
|
|
|
struct InputEvent
|
|
{
|
|
Input key;
|
|
union
|
|
{
|
|
struct
|
|
{
|
|
bool pressed;
|
|
};
|
|
struct
|
|
{
|
|
i32 rel_x;
|
|
i32 rel_y;
|
|
u16 x;
|
|
u16 y;
|
|
};
|
|
};
|
|
}
|
|
|
|
struct Inputs
|
|
{
|
|
DLList!(InputEvent) list;
|
|
Arena arena;
|
|
}
|
|
|
|
void
|
|
ResetInputs(Inputs* inputs)
|
|
{
|
|
inputs.list.first = inputs.list.last = null;
|
|
Reset(&inputs.arena);
|
|
}
|
|
|
|
void
|
|
Push(Inputs* inputs, Input input, bool pressed)
|
|
{
|
|
DNode!(InputEvent)* node = Alloc!(DNode!(InputEvent))(&inputs.arena);
|
|
node.value.key = input;
|
|
node.value.pressed = pressed;
|
|
|
|
DLLPushFront(&inputs.list, node, null);
|
|
}
|
|
|
|
void
|
|
Push(Inputs* inputs, i32 rel_x, i32 rel_y, u16 x, u16 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;
|
|
|
|
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;
|
|
Inputs inputs;
|
|
};
|
|
|
|
struct Library
|
|
{
|
|
void* ptr;
|
|
};
|
|
|
|
struct Function
|
|
{
|
|
void* ptr;
|
|
};
|
|
|
|
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,
|
|
inputs: {
|
|
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);
|
|
|
|
LockCursor(&window);
|
|
UnlockCursor(&window);
|
|
|
|
return window;
|
|
};
|
|
|
|
void
|
|
LockCursor(PlatformWindow* window)
|
|
{
|
|
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)
|
|
{
|
|
break;
|
|
}
|
|
|
|
assert(counter < 5, "Unable to grab cursor");
|
|
counter += 1;
|
|
Thread.sleep(dur!("msecs")(50));
|
|
}
|
|
|
|
HideCursor(window);
|
|
window.locked_cursor = true;
|
|
}
|
|
}
|
|
|
|
void
|
|
UnlockCursor(PlatformWindow* window)
|
|
{
|
|
if (window.locked_cursor)
|
|
{
|
|
xcb_ungrab_pointer(window.conn, XCB_CURRENT_TIME);
|
|
ShowCursor(window);
|
|
window.locked_cursor = false;
|
|
}
|
|
}
|
|
|
|
// TODO: improve error handling for show/hide cursor
|
|
void
|
|
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);
|
|
CheckErr(window, &hide_cursor_cookie, error, "xcb_xfixes_hide_cursor_checked failure");
|
|
pureFree(error);
|
|
}
|
|
|
|
void
|
|
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);
|
|
CheckErr(window, &show_cursor_cookie, error, "xcb_xfixes_show_cursor_checked failure");
|
|
pureFree(error);
|
|
}
|
|
|
|
void
|
|
FlushEvents(PlatformWindow* window)
|
|
{
|
|
xcb_generic_event_t* e;
|
|
do
|
|
{
|
|
e = xcb_poll_for_event(window.conn);
|
|
} while (e);
|
|
}
|
|
|
|
void
|
|
HandleEvents(PlatformWindow* window)
|
|
{
|
|
xcb_generic_event_t* e;
|
|
|
|
bool ignore_mouse_events = false;
|
|
|
|
Inputs* inputs = &window.inputs;
|
|
ResetInputs(inputs);
|
|
|
|
do
|
|
{
|
|
e = xcb_poll_for_event(window.conn);
|
|
|
|
if (e)
|
|
{
|
|
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 != window.window)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (msg.data.data32[0] == window.close_event)
|
|
{
|
|
window.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(window.display, cast(KeyCode)code, 0, 0);
|
|
KBI input = ConvertInput(key_sym);
|
|
|
|
if (input != KBI.None)
|
|
{
|
|
Push(inputs, input, pressed);
|
|
}
|
|
} 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, pressed);
|
|
}
|
|
} 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)
|
|
{
|
|
window.mouse_prev_x = x;
|
|
window.mouse_prev_y = y;
|
|
first = false;
|
|
}
|
|
|
|
if (x > 0 || y > 0)
|
|
{
|
|
Push(inputs, window.mouse_prev_x-x, window.mouse_prev_y-y, x, y);
|
|
}
|
|
|
|
window.mouse_prev_x = x;
|
|
window.mouse_prev_y = y;
|
|
|
|
if (window.locked_cursor &&
|
|
(x < WINDOW_EDGE_BUFFER || y < WINDOW_EDGE_BUFFER || x > window.w - WINDOW_EDGE_BUFFER || y > window.h - WINDOW_EDGE_BUFFER))
|
|
{
|
|
i16 new_x = cast(i16)(window.w / 2);
|
|
i16 new_y = cast(i16)(window.h / 2);
|
|
xcb_warp_pointer(window.conn, window.window, window.window, 0, 0, cast(i16)window.w, cast(i16)window.h, new_x, new_y);
|
|
window.mouse_prev_x = new_x;
|
|
window.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 (window.w != config_event.width || window.h != config_event.height)
|
|
{
|
|
window.w = config_event.width;
|
|
window.h = config_event.height;
|
|
}
|
|
} break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
while (e);
|
|
}
|
|
|
|
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 KBI.Backspace;
|
|
case XK_Return: return KBI.Enter;
|
|
case XK_Tab: return KBI.Tab;
|
|
case XK_Pause: return KBI.Pause;
|
|
case XK_Caps_Lock: return KBI.CapsLock;
|
|
case XK_Escape: return KBI.Esc;
|
|
case XK_space: return KBI.Space;
|
|
case XK_Prior: return KBI.PageUp;
|
|
case XK_Next: return KBI.PageDown;
|
|
case XK_End: return KBI.End;
|
|
case XK_Home: return KBI.Home;
|
|
case XK_Left: return KBI.Left;
|
|
case XK_Up: return KBI.Up;
|
|
case XK_Right: return KBI.Right;
|
|
case XK_Down: return KBI.Down;
|
|
case XK_Print: return KBI.PrintScreen;
|
|
case XK_Insert: return KBI.Insert;
|
|
case XK_Delete: return KBI.Delete;
|
|
case XK_Meta_L:
|
|
case XK_Super_L: return KBI.LeftSuper;
|
|
case XK_Meta_R:
|
|
case XK_Super_R: return KBI.RightSuper;
|
|
case XK_KP_0: return KBI.Num0;
|
|
case XK_KP_1: return KBI.Num1;
|
|
case XK_KP_2: return KBI.Num2;
|
|
case XK_KP_3: return KBI.Num3;
|
|
case XK_KP_4: return KBI.Num4;
|
|
case XK_KP_5: return KBI.Num5;
|
|
case XK_KP_6: return KBI.Num6;
|
|
case XK_KP_7: return KBI.Num7;
|
|
case XK_KP_8: return KBI.Num8;
|
|
case XK_KP_9: return KBI.Num9;
|
|
case XK_multiply: return KBI.NumStar;
|
|
case XK_KP_Subtract: return KBI.NumMinus;
|
|
case XK_KP_Decimal: return KBI.NumPeriod;
|
|
case XK_KP_Divide: return KBI.NumSlash;
|
|
case XK_KP_Add: return KBI.NumPlus;
|
|
case XK_F1: return KBI.F1;
|
|
case XK_F2: return KBI.F2;
|
|
case XK_F3: return KBI.F3;
|
|
case XK_F4: return KBI.F4;
|
|
case XK_F5: return KBI.F5;
|
|
case XK_F6: return KBI.F6;
|
|
case XK_F7: return KBI.F7;
|
|
case XK_F8: return KBI.F8;
|
|
case XK_F9: return KBI.F9;
|
|
case XK_F10: return KBI.F10;
|
|
case XK_F11: return KBI.F11;
|
|
case XK_F12: return KBI.F12;
|
|
case XK_Num_Lock: return KBI.NumLock;
|
|
case XK_Scroll_Lock: return KBI.ScrollLock;
|
|
case XK_Shift_L: return KBI.LeftShift;
|
|
case XK_Shift_R: return KBI.RightShift;
|
|
case XK_Control_L: return KBI.LeftCtrl;
|
|
case XK_Control_R: return KBI.RightCtrl;
|
|
case XK_Alt_L: return KBI.LeftAlt;
|
|
case XK_Alt_R: return KBI.RightAlt;
|
|
case XK_semicolon: return KBI.Semicolon;
|
|
case XK_bracketleft: return KBI.LeftBrace;
|
|
case XK_bracketright: return KBI.RightBrace;
|
|
case XK_plus: return KBI.Plus;
|
|
case XK_comma: return KBI.Comma;
|
|
case XK_minus: return KBI.Minus;
|
|
case XK_backslash: return KBI.BackSlash;
|
|
case XK_slash: return KBI.ForwardSlash;
|
|
case XK_grave: return KBI.Tilde;
|
|
case XK_0: return KBI.Zero;
|
|
case XK_1: return KBI.One;
|
|
case XK_2: return KBI.Two;
|
|
case XK_3: return KBI.Three;
|
|
case XK_4: return KBI.Four;
|
|
case XK_5: return KBI.Five;
|
|
case XK_6: return KBI.Six;
|
|
case XK_7: return KBI.Seven;
|
|
case XK_8: return KBI.Eight;
|
|
case XK_9: return KBI.Nine;
|
|
case XK_a:
|
|
case XK_A: return KBI.A;
|
|
case XK_b:
|
|
case XK_B: return KBI.B;
|
|
case XK_c:
|
|
case XK_C: return KBI.C;
|
|
case XK_d:
|
|
case XK_D: return KBI.D;
|
|
case XK_e:
|
|
case XK_E: return KBI.E;
|
|
case XK_f:
|
|
case XK_F: return KBI.F;
|
|
case XK_g:
|
|
case XK_G: return KBI.G;
|
|
case XK_h:
|
|
case XK_H: return KBI.H;
|
|
case XK_i:
|
|
case XK_I: return KBI.I;
|
|
case XK_j:
|
|
case XK_J: return KBI.J;
|
|
case XK_k:
|
|
case XK_K: return KBI.K;
|
|
case XK_l:
|
|
case XK_L: return KBI.L;
|
|
case XK_m:
|
|
case XK_M: return KBI.M;
|
|
case XK_n:
|
|
case XK_N: return KBI.N;
|
|
case XK_o:
|
|
case XK_O: return KBI.O;
|
|
case XK_p:
|
|
case XK_P: return KBI.P;
|
|
case XK_q:
|
|
case XK_Q: return KBI.Q;
|
|
case XK_r:
|
|
case XK_R: return KBI.R;
|
|
case XK_s:
|
|
case XK_S: return KBI.S;
|
|
case XK_t:
|
|
case XK_T: return KBI.T;
|
|
case XK_u:
|
|
case XK_U: return KBI.U;
|
|
case XK_v:
|
|
case XK_V: return KBI.V;
|
|
case XK_w:
|
|
case XK_W: return KBI.W;
|
|
case XK_x:
|
|
case XK_X: return KBI.X;
|
|
case XK_y:
|
|
case XK_Y: return KBI.Y;
|
|
case XK_z:
|
|
case XK_Z: return KBI.Z;
|
|
default: return KBI.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)
|
|
{
|
|
|
|
}
|