diff --git a/.gitignore b/.gitignore
index 3d33258..07056f0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-editor
+demo
 *.o
 fd-*.h
 imgui.ini
diff --git a/.vim.local/ycm_extra_conf.py b/.vim.local/ycm_extra_conf.py
index 1567cb3..bb8b71c 100644
--- a/.vim.local/ycm_extra_conf.py
+++ b/.vim.local/ycm_extra_conf.py
@@ -46,9 +46,7 @@ flags = [
     'c++',
     '-I','.',
     '-I','imgui',
-    '-I','imguifs',
     '-I','glm/glm',
-    '-I','picojson',
     '-I','/usr/include/SDL2'
 ]
 
diff --git a/Makefile b/Makefile
index cb5ffe7..00327a3 100644
--- a/Makefile
+++ b/Makefile
@@ -1,36 +1,29 @@
 CXXFLAGS += $(shell sdl2-config --cflags) -std=c++14 -Wall
 CFLAGS += $(shell sdl2-config --cflags)
-CPPFLAGS += -I. -Iimgui -Iimguifs -Iglm -Ipicojson \
+CPPFLAGS += -I. -Iimgui -Iglm \
 	    -DREAL_BUILD -DGLM_ENABLE_EXPERIMENTAL
 LIBS += $(shell sdl2-config --libs) -lGL -lGLEW -ldl
 
-FILEDUMPS = \
-	    fd-raymarcher.glsl.h
+FILEDUMPS =
 
-IMGUI = imgui.o imgui_demo.o imgui_draw.o imguifs.o
-EDITOR = \
-	 editor.o \
+IMGUI = imgui.o imgui_demo.o imgui_draw.o
+DEMO = \
+	 main.o \
 	 imgui_impl_sdl.o \
-	 utilities.o \
-	 project.o \
-	 \
-	 mg-none.o \
-	 mg-fragment.o \
-	 \
-	 gfx-raymarcher.o
+	 utilities.o
 
-EDITOR_DEPS = $(EDITOR:%.o=.%.d)
+DEMO_DEPS = $(DEMO:%.o=.%.d)
 
 
-editor:	$(EDITOR) $(IMGUI)
-	$(CXX) $(CXXFLAGS) $(CPPFLAGS) -o editor \
-		$(EDITOR) $(IMGUI) $(LIBS)
+demo:	$(DEMO) $(IMGUI)
+	$(CXX) $(CXXFLAGS) $(CPPFLAGS) -o demo \
+		$(DEMO) $(IMGUI) $(LIBS)
 
 clean:
-	rm -f $(IMGUI) $(EDITOR) $(FILEDUMPS) editor externals.hh.gch
+	rm -f $(IMGUI) $(DEMO) $(FILEDUMPS) demo externals.hh.gch
 
 fullclean: clean
-	rm -f $(EDITOR_DEPS)
+	rm -f $(DEMO_DEPS)
 
 .PHONY: clean fullclean
 
@@ -48,13 +41,9 @@ imgui_demo.o: imgui/imgui_demo.cpp
 imgui_draw.o: imgui/imgui_draw.cpp
 	$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c -o $@ $<
 
-imguifs.o: imguifs/imguifilesystem.cpp imguifs/dirent_portable.h imguifs/imguifilesystem.h
-	$(CXX) $(CXXFLAGS) $(CPPFLAGS) -include /usr/include/stdlib.h -c -o $@ $<
+-include $(DEMO_DEPS)
 
-
--include $(EDITOR_DEPS)
-
-$(EDITOR): %.o: %.cc externals.hh.gch | $(FILEDUMPS)
+$(DEMO): %.o: %.cc externals.hh.gch | $(FILEDUMPS)
 	$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c -o $@ $<
 	$(CXX) $(CXXFLAGS) $(CPPFLAGS) -M -MF $(@:%.o=.%.d) -MT $@ $<
 
diff --git a/externals.hh b/externals.hh
new file mode 100644
index 0000000..1e4f849
--- /dev/null
+++ b/externals.hh
@@ -0,0 +1,32 @@
+// C
+#include <stdio.h>
+
+// System (C)
+#include <sys/inotify.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <unistd.h>
+
+// Misc (C)
+#include <SDL.h>
+#include <GL/glew.h>
+
+// C++ std
+#include <vector>
+#include <string>
+#include <algorithm>
+#include <functional>
+#include <memory>
+#include <fstream>
+
+// ImGui
+#include <imgui.h>
+
+// GLM
+#include <glm/mat3x3.hpp>
+#include <glm/gtx/euler_angles.hpp>
+
+// Silly decoration macros I use everywhere
+#define __rd__
+#define __wr__
+#define __rw__
diff --git a/imgui_impl_sdl.cc b/imgui_impl_sdl.cc
new file mode 100644
index 0000000..c089c6f
--- /dev/null
+++ b/imgui_impl_sdl.cc
@@ -0,0 +1,282 @@
+// ImGui SDL2 binding with OpenGL
+// In this binding, ImTextureID is used to store an OpenGL 'GLuint' texture identifier. Read the FAQ about ImTextureID in imgui.cpp.
+
+// You can copy and use unmodified imgui_impl_* files in your project. See main.cpp for an example of using this.
+// If you use this binding you'll need to call 4 functions: ImGui_ImplXXXX_Init(), ImGui_ImplXXXX_NewFrame(), ImGui::Render() and ImGui_ImplXXXX_Shutdown().
+// If you are new to ImGui, see examples/README.txt and documentation at the top of imgui.cpp.
+// https://github.com/ocornut/imgui
+
+#include "externals.hh"
+#include "imgui_impl_sdl.h"
+
+// Data
+static double       g_Time = 0.0f;
+static bool         g_MousePressed[3] = { false, false, false };
+static float        g_MouseWheel = 0.0f;
+static GLuint       g_FontTexture = 0;
+
+// This is the main rendering function that you have to implement and provide to ImGui (via setting up 'RenderDrawListsFn' in the ImGuiIO structure)
+// If text or lines are blurry when integrating ImGui in your engine:
+// - in your Render function, try translating your projection matrix by (0.5f,0.5f) or (0.375f,0.375f)
+void ImGui_ImplSdl_RenderDrawLists(ImDrawData* draw_data)
+{
+    // Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates)
+    ImGuiIO& io = ImGui::GetIO();
+    int fb_width = (int)(io.DisplaySize.x * io.DisplayFramebufferScale.x);
+    int fb_height = (int)(io.DisplaySize.y * io.DisplayFramebufferScale.y);
+    if (fb_width == 0 || fb_height == 0)
+        return;
+    draw_data->ScaleClipRects(io.DisplayFramebufferScale);
+
+    // We are using the OpenGL fixed pipeline to make the example code simpler to read!
+    // Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, vertex/texcoord/color pointers.
+    GLint last_texture; glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture);
+    GLint last_viewport[4]; glGetIntegerv(GL_VIEWPORT, last_viewport);
+    GLint last_scissor_box[4]; glGetIntegerv(GL_SCISSOR_BOX, last_scissor_box); 
+    glPushAttrib(GL_ENABLE_BIT | GL_COLOR_BUFFER_BIT | GL_TRANSFORM_BIT);
+    glEnable(GL_BLEND);
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+    glDisable(GL_CULL_FACE);
+    glDisable(GL_DEPTH_TEST);
+    glEnable(GL_SCISSOR_TEST);
+    glEnableClientState(GL_VERTEX_ARRAY);
+    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
+    glEnableClientState(GL_COLOR_ARRAY);
+    glEnable(GL_TEXTURE_2D);
+    //glUseProgram(0); // You may want this if using this code in an OpenGL 3+ context
+
+    // Setup viewport, orthographic projection matrix
+    glViewport(0, 0, (GLsizei)fb_width, (GLsizei)fb_height);
+    glMatrixMode(GL_PROJECTION);
+    glPushMatrix();
+    glLoadIdentity();
+    glOrtho(0.0f, io.DisplaySize.x, io.DisplaySize.y, 0.0f, -1.0f, +1.0f);
+    glMatrixMode(GL_MODELVIEW);
+    glPushMatrix();
+    glLoadIdentity();
+
+    // Render command lists
+    #define OFFSETOF(TYPE, ELEMENT) ((size_t)&(((TYPE *)0)->ELEMENT))
+    for (int n = 0; n < draw_data->CmdListsCount; n++)
+    {
+        const ImDrawList* cmd_list = draw_data->CmdLists[n];
+        const ImDrawVert* vtx_buffer = cmd_list->VtxBuffer.Data;
+        const ImDrawIdx* idx_buffer = cmd_list->IdxBuffer.Data;
+        glVertexPointer(2, GL_FLOAT, sizeof(ImDrawVert), (const GLvoid*)((const char*)vtx_buffer + OFFSETOF(ImDrawVert, pos)));
+        glTexCoordPointer(2, GL_FLOAT, sizeof(ImDrawVert), (const GLvoid*)((const char*)vtx_buffer + OFFSETOF(ImDrawVert, uv)));
+        glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(ImDrawVert), (const GLvoid*)((const char*)vtx_buffer + OFFSETOF(ImDrawVert, col)));
+
+        for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++)
+        {
+            const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i];
+            if (pcmd->UserCallback)
+            {
+                pcmd->UserCallback(cmd_list, pcmd);
+            }
+            else
+            {
+                glBindTexture(GL_TEXTURE_2D, (GLuint)(intptr_t)pcmd->TextureId);
+                glScissor((int)pcmd->ClipRect.x, (int)(fb_height - pcmd->ClipRect.w), (int)(pcmd->ClipRect.z - pcmd->ClipRect.x), (int)(pcmd->ClipRect.w - pcmd->ClipRect.y));
+                glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer);
+            }
+            idx_buffer += pcmd->ElemCount;
+        }
+    }
+    #undef OFFSETOF
+
+    // Restore modified state
+    glDisableClientState(GL_COLOR_ARRAY);
+    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
+    glDisableClientState(GL_VERTEX_ARRAY);
+    glBindTexture(GL_TEXTURE_2D, (GLuint)last_texture);
+    glMatrixMode(GL_MODELVIEW);
+    glPopMatrix();
+    glMatrixMode(GL_PROJECTION);
+    glPopMatrix();
+    glPopAttrib();
+    glViewport(last_viewport[0], last_viewport[1], (GLsizei)last_viewport[2], (GLsizei)last_viewport[3]);
+    glScissor(last_scissor_box[0], last_scissor_box[1], (GLsizei)last_scissor_box[2], (GLsizei)last_scissor_box[3]);
+}
+
+static const char* ImGui_ImplSdl_GetClipboardText(void*)
+{
+    return SDL_GetClipboardText();
+}
+
+static void ImGui_ImplSdl_SetClipboardText(void*, const char* text)
+{
+    SDL_SetClipboardText(text);
+}
+
+bool ImGui_ImplSdl_ProcessEvent(SDL_Event* event)
+{
+    ImGuiIO& io = ImGui::GetIO();
+    switch (event->type)
+    {
+    case SDL_MOUSEWHEEL:
+        {
+            if (event->wheel.y > 0)
+                g_MouseWheel = 1;
+            if (event->wheel.y < 0)
+                g_MouseWheel = -1;
+            return true;
+        }
+    case SDL_MOUSEBUTTONDOWN:
+        {
+            if (event->button.button == SDL_BUTTON_LEFT) g_MousePressed[0] = true;
+            if (event->button.button == SDL_BUTTON_RIGHT) g_MousePressed[1] = true;
+            if (event->button.button == SDL_BUTTON_MIDDLE) g_MousePressed[2] = true;
+            return true;
+        }
+    case SDL_TEXTINPUT:
+        {
+            io.AddInputCharactersUTF8(event->text.text);
+            return true;
+        }
+    case SDL_KEYDOWN:
+    case SDL_KEYUP:
+        {
+            int key = event->key.keysym.sym & ~SDLK_SCANCODE_MASK;
+            io.KeysDown[key] = (event->type == SDL_KEYDOWN);
+            io.KeyShift = ((SDL_GetModState() & KMOD_SHIFT) != 0);
+            io.KeyCtrl = ((SDL_GetModState() & KMOD_CTRL) != 0);
+            io.KeyAlt = ((SDL_GetModState() & KMOD_ALT) != 0);
+            io.KeySuper = ((SDL_GetModState() & KMOD_GUI) != 0);
+            return true;
+        }
+    }
+    return false;
+}
+
+bool ImGui_ImplSdl_CreateDeviceObjects()
+{
+    // Build texture atlas
+    ImGuiIO& io = ImGui::GetIO();
+    unsigned char* pixels;
+    int width, height;
+    io.Fonts->GetTexDataAsAlpha8(&pixels, &width, &height);
+
+    // Upload texture to graphics system
+    GLint last_texture;
+    glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture);
+    glGenTextures(1, &g_FontTexture);
+    glBindTexture(GL_TEXTURE_2D, g_FontTexture);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, width, height, 0, GL_ALPHA, GL_UNSIGNED_BYTE, pixels);
+
+    // Store our identifier
+    io.Fonts->TexID = (void *)(intptr_t)g_FontTexture;
+
+    // Restore state
+    glBindTexture(GL_TEXTURE_2D, last_texture);
+
+    return true;
+}
+
+void    ImGui_ImplSdl_InvalidateDeviceObjects()
+{
+    if (g_FontTexture)
+    {
+        glDeleteTextures(1, &g_FontTexture);
+        ImGui::GetIO().Fonts->TexID = 0;
+        g_FontTexture = 0;
+    }
+}
+
+bool    ImGui_ImplSdl_Init(SDL_Window* window)
+{
+    ImGuiIO& io = ImGui::GetIO();
+    io.KeyMap[ImGuiKey_Tab] = SDLK_TAB;                     // Keyboard mapping. ImGui will use those indices to peek into the io.KeyDown[] array.
+    io.KeyMap[ImGuiKey_LeftArrow] = SDL_SCANCODE_LEFT;
+    io.KeyMap[ImGuiKey_RightArrow] = SDL_SCANCODE_RIGHT;
+    io.KeyMap[ImGuiKey_UpArrow] = SDL_SCANCODE_UP;
+    io.KeyMap[ImGuiKey_DownArrow] = SDL_SCANCODE_DOWN;
+    io.KeyMap[ImGuiKey_PageUp] = SDL_SCANCODE_PAGEUP;
+    io.KeyMap[ImGuiKey_PageDown] = SDL_SCANCODE_PAGEDOWN;
+    io.KeyMap[ImGuiKey_Home] = SDL_SCANCODE_HOME;
+    io.KeyMap[ImGuiKey_End] = SDL_SCANCODE_END;
+    io.KeyMap[ImGuiKey_Delete] = SDLK_DELETE;
+    io.KeyMap[ImGuiKey_Backspace] = SDLK_BACKSPACE;
+    io.KeyMap[ImGuiKey_Enter] = SDLK_RETURN;
+    io.KeyMap[ImGuiKey_Escape] = SDLK_ESCAPE;
+    io.KeyMap[ImGuiKey_A] = SDLK_a;
+    io.KeyMap[ImGuiKey_C] = SDLK_c;
+    io.KeyMap[ImGuiKey_V] = SDLK_v;
+    io.KeyMap[ImGuiKey_X] = SDLK_x;
+    io.KeyMap[ImGuiKey_Y] = SDLK_y;
+    io.KeyMap[ImGuiKey_Z] = SDLK_z;
+
+    io.RenderDrawListsFn = ImGui_ImplSdl_RenderDrawLists;   // Alternatively you can set this to NULL and call ImGui::GetDrawData() after ImGui::Render() to get the same ImDrawData pointer.
+    io.SetClipboardTextFn = ImGui_ImplSdl_SetClipboardText;
+    io.GetClipboardTextFn = ImGui_ImplSdl_GetClipboardText;
+    io.ClipboardUserData = NULL;
+
+#ifdef _WIN32
+    SDL_SysWMinfo wmInfo;
+    SDL_VERSION(&wmInfo.version);
+    SDL_GetWindowWMInfo(window, &wmInfo);
+    io.ImeWindowHandle = wmInfo.info.win.window;
+#else
+    (void)window;
+#endif
+
+    return true;
+}
+
+void ImGui_ImplSdl_Shutdown()
+{
+    ImGui_ImplSdl_InvalidateDeviceObjects();
+    ImGui::Shutdown();
+}
+
+void ImGui_ImplSdl_NewFrame(SDL_Window *window ,
+		bool mouseLock,ImVec2 const& mousePos)
+{
+    if (!g_FontTexture)
+        ImGui_ImplSdl_CreateDeviceObjects();
+
+    ImGuiIO& io = ImGui::GetIO();
+
+    // Setup display size (every frame to accommodate for window resizing)
+    int w, h;
+    int display_w, display_h;
+    SDL_GetWindowSize(window, &w, &h);
+    SDL_GL_GetDrawableSize(window, &display_w, &display_h);
+    io.DisplaySize = ImVec2((float)w, (float)h);
+    io.DisplayFramebufferScale = ImVec2(w > 0 ? ((float)display_w / w) : 0, h > 0 ? ((float)display_h / h) : 0);
+
+    // Setup time step
+    Uint32	time = SDL_GetTicks();
+    double current_time = time / 1000.0;
+    io.DeltaTime = g_Time > 0.0 ? (float)(current_time - g_Time) : (float)(1.0f/60.0f);
+    g_Time = current_time;
+
+    // Setup inputs
+    // (we already got mouse wheel, keyboard keys & characters from SDL_PollEvent())
+    int mx, my;
+    Uint32 mouseMask = SDL_GetMouseState(&mx, &my);
+    if ( mouseLock ) {
+        io.MousePos = mousePos;
+    } else {
+        if (SDL_GetWindowFlags(window) & SDL_WINDOW_MOUSE_FOCUS)
+            io.MousePos = ImVec2((float)mx, (float)my);   // Mouse position, in pixels (set to -1,-1 if no mouse / on another screen, etc.)
+        else
+            io.MousePos = ImVec2(-1,-1);
+    }
+
+    io.MouseDown[0] = g_MousePressed[0] || (mouseMask & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0;		// If a mouse press event came, always pass it as "mouse held this frame", so we don't miss click-release events that are shorter than 1 frame.
+    io.MouseDown[1] = g_MousePressed[1] || (mouseMask & SDL_BUTTON(SDL_BUTTON_RIGHT)) != 0;
+    io.MouseDown[2] = g_MousePressed[2] || (mouseMask & SDL_BUTTON(SDL_BUTTON_MIDDLE)) != 0;
+    g_MousePressed[0] = g_MousePressed[1] = g_MousePressed[2] = false;
+
+    io.MouseWheel = g_MouseWheel;
+    g_MouseWheel = 0.0f;
+
+    // Hide OS mouse cursor if ImGui is drawing it
+    SDL_ShowCursor(io.MouseDrawCursor ? 0 : 1);
+
+    // Start the frame
+    ImGui::NewFrame();
+}
diff --git a/imgui_impl_sdl.h b/imgui_impl_sdl.h
new file mode 100644
index 0000000..07fe809
--- /dev/null
+++ b/imgui_impl_sdl.h
@@ -0,0 +1,20 @@
+// ImGui SDL2 binding with OpenGL
+// In this binding, ImTextureID is used to store an OpenGL 'GLuint' texture identifier. Read the FAQ about ImTextureID in imgui.cpp.
+
+// You can copy and use unmodified imgui_impl_* files in your project. See main.cpp for an example of using this.
+// If you use this binding you'll need to call 4 functions: ImGui_ImplXXXX_Init(), ImGui_ImplXXXX_NewFrame(), ImGui::Render() and ImGui_ImplXXXX_Shutdown().
+// If you are new to ImGui, see examples/README.txt and documentation at the top of imgui.cpp.
+// https://github.com/ocornut/imgui
+
+struct SDL_Window;
+typedef union SDL_Event SDL_Event;
+
+IMGUI_API bool        ImGui_ImplSdl_Init(SDL_Window* window);
+IMGUI_API void        ImGui_ImplSdl_Shutdown();
+IMGUI_API void        ImGui_ImplSdl_NewFrame(SDL_Window* window,
+		bool mouseLock,ImVec2 const& mousePos);
+IMGUI_API bool        ImGui_ImplSdl_ProcessEvent(SDL_Event* event);
+
+// Use if you want to reset your rendering device without losing ImGui state.
+IMGUI_API void        ImGui_ImplSdl_InvalidateDeviceObjects();
+IMGUI_API bool        ImGui_ImplSdl_CreateDeviceObjects();
diff --git a/main.cc b/main.cc
new file mode 100644
index 0000000..6ad047d
--- /dev/null
+++ b/main.cc
@@ -0,0 +1,179 @@
+#include "externals.hh"
+
+#include "imgui_impl_sdl.h"
+#include "utilities.hh"
+
+
+/*= T_Main ===================================================================*/
+
+struct T_Main
+{
+	T_Main( );
+	~T_Main( );
+
+	void mainLoop( );
+
+    private:
+	const std::string projectFile;
+	SDL_Window * window;
+	SDL_GLContext gl;
+	T_FilesWatcher watcher;
+	std::string loadError;
+
+	bool done = false;
+	bool capture = false;
+	ImVec2 mouseInitial;
+	ImVec2 mouseMove;
+
+	void startIteration( );
+	void handleCapture( );
+	void makeUI( );
+	void render( );
+};
+
+/*----------------------------------------------------------------------------*/
+
+T_Main::T_Main( )
+{
+	SDL_Init( SDL_INIT_VIDEO | SDL_INIT_TIMER );
+
+	// Setup window
+	SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER , 1 );
+	SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE , 24 );
+	SDL_GL_SetAttribute( SDL_GL_STENCIL_SIZE , 8 );
+	SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION , 2 );
+	SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION , 2 );
+	SDL_DisplayMode current;
+	SDL_GetCurrentDisplayMode( 0 , &current );
+	window = SDL_CreateWindow( "DEMO",
+			SDL_WINDOWPOS_CENTERED , SDL_WINDOWPOS_CENTERED ,
+			1280 , 720 ,
+			SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE );
+	gl = SDL_GL_CreateContext( window );
+	glewInit();
+	ImGui_ImplSdl_Init( window );
+}
+
+void T_Main::mainLoop( )
+{
+	while ( !done ) {
+		startIteration( );
+		if ( !done ) {
+			handleCapture( );
+			makeUI( );
+			watcher.check( );
+			render( );
+		}
+	}
+}
+
+T_Main::~T_Main( )
+{
+	ImGui_ImplSdl_Shutdown( );
+	SDL_GL_DeleteContext( gl );
+	SDL_DestroyWindow( window );
+	SDL_Quit( );
+}
+
+/*----------------------------------------------------------------------------*/
+
+void T_Main::startIteration( )
+{
+	SDL_Event event;
+	mouseMove = ImVec2( );
+	while ( SDL_PollEvent( &event ) ) {
+		ImGui_ImplSdl_ProcessEvent( &event );
+		if ( event.type == SDL_QUIT ) {
+			done = true;
+			return;
+		}
+
+		if ( capture && event.type == SDL_MOUSEMOTION ) {
+			mouseMove.x += event.motion.xrel;
+			mouseMove.y += event.motion.yrel;
+		}
+	}
+
+	ImGui_ImplSdl_NewFrame( window , capture , mouseInitial );
+	ImGui::GetIO( ).MouseDrawCursor = true;
+}
+
+void T_Main::handleCapture( )
+{
+	auto const& io( ImGui::GetIO( ) );
+	const bool lmb( ImGui::IsMouseDown( 0 ) );
+	const bool mb( lmb || ImGui::IsMouseDown( 1 ) );
+	const bool appCanGrab( !( ImGui::IsMouseHoveringAnyWindow( )
+				|| io.WantCaptureMouse
+				|| io.WantCaptureKeyboard ) );
+	const bool shift( io.KeyShift );
+	const bool ctrl( io.KeyCtrl );
+
+	if ( capture && !mb ) {
+		capture = false;
+		ImGui::CaptureMouseFromApp( false );
+		SDL_SetRelativeMouseMode( SDL_FALSE );
+		SDL_WarpMouseInWindow( window ,
+				int( mouseInitial.x ) ,
+				int( mouseInitial.y ) );
+		ImGui::SetMouseCursor( ImGuiMouseCursor_Arrow );
+	} else if ( capture ) {
+		ImGui::SetMouseCursor( ImGuiMouseCursor_Move );
+		//project->handleDND( mouseMove , ctrl , shift , lmb );
+		// FIXME: D'n'D
+	} else if ( appCanGrab && mb ) {
+		capture = true;
+		mouseInitial = ImGui::GetMousePos( );
+		ImGui::CaptureMouseFromApp( true );
+		SDL_SetRelativeMouseMode( SDL_TRUE );
+		ImGui::SetMouseCursor( ImGuiMouseCursor_Move );
+	}
+
+	if ( ( appCanGrab || capture ) && io.MouseWheel ) {
+		//project->handleWheel( io.MouseWheel , ctrl , shift );
+		// FIXME wheel
+	}
+}
+
+void T_Main::makeUI( )
+{
+}
+
+void T_Main::render( )
+{
+	auto const& dspSize( ImGui::GetIO( ).DisplaySize );
+	glViewport( 0 , 0 , (int) dspSize.x, (int) dspSize.y );
+	glClearColor( 1 , 0 , 1 , 1 );
+	glClear( GL_COLOR_BUFFER_BIT );
+
+	// FIXME draw the fuck
+	//project->render( );
+
+	glUseProgram( 0 );
+	ImGui::Render( );
+	SDL_GL_SwapWindow( window );
+}
+
+
+/*============================================================================*/
+
+int main( int , char** )
+{
+	T_Main m;
+	m.mainLoop( );
+
+#if 0
+	// Frame time history
+	const int nFrameTimes = 200;
+	float frameTimes[ nFrameTimes ];
+	memset( frameTimes , 0 , sizeof( float ) * nFrameTimes );
+#endif
+#if 0
+		// Update frame time history
+		memmove( frameTimes , &frameTimes[ 1 ] ,
+				( nFrameTimes - 1 ) * sizeof( float ) );
+		frameTimes[ nFrameTimes - 1 ] = 1000.0f / ImGui::GetIO( ).Framerate;
+#endif
+
+	return 0;
+}
diff --git a/utilities.cc b/utilities.cc
new file mode 100644
index 0000000..429ebd6
--- /dev/null
+++ b/utilities.cc
@@ -0,0 +1,379 @@
+#include "externals.hh"
+#include "utilities.hh"
+
+
+void disableButton( )
+{
+	ImGui::PushStyleColor( ImGuiCol_Button , ImColor( .3f , .3f , .3f ) );
+	ImGui::PushStyleColor( ImGuiCol_ButtonHovered , ImColor( .3f , .3f , .3f ) );
+	ImGui::PushStyleColor( ImGuiCol_ButtonActive , ImColor( .3f , .3f , .3f ) );
+}
+
+/*----------------------------------------------------------------------------*/
+
+void updateAngle(
+		__rw__	float& initial ,
+		__rd__	const float delta
+	)
+{
+	initial = fmod( initial + delta + 540 , 360 ) - 180;
+}
+
+void anglesToMatrix(
+		__rd__	float const* angles ,
+		__wr__	float* matrix )
+{
+	float c[3] , s[3];
+	for ( int i = 0 ; i < 3 ; i ++ ) {
+		const float a = M_PI * angles[ i ] / 180;
+		c[i] = cos( a );
+		s[i] = sin( a );
+	}
+	matrix[0] = c[1]*c[2];
+	matrix[1] = s[0]*s[1]*c[2] - c[0]*s[2];
+	matrix[2] = s[0]*s[2] + c[0]*s[1]*c[2];
+	matrix[3] = c[1]*s[2];
+	matrix[4] = c[0]*c[2] + s[0]*s[1]*s[2];
+	matrix[5] = c[0]*s[1]*s[2] - s[0]*c[2];
+	matrix[6] = -s[1];
+	matrix[7] = s[0]*c[1];
+	matrix[8] = c[0]*c[1];
+}
+
+
+/*= T_FilesWatcher ===========================================================*/
+
+T_FilesWatcher::T_FilesWatcher( )
+	: fd( inotify_init1( O_NONBLOCK ) )
+{ }
+
+T_FilesWatcher::T_FilesWatcher( T_FilesWatcher&& other ) noexcept
+	: fd( 0 ) , watched( std::move( other.watched ) )
+{
+	std::swap( fd , other.fd );
+	other.watched.clear( );
+	for ( T_WatchedFiles* wf : watched ) {
+		if ( wf ) {
+			wf->watcher = this;
+		}
+	}
+}
+
+T_FilesWatcher::~T_FilesWatcher( )
+{
+	if ( fd ) {
+		close( fd );
+	}
+	for ( T_WatchedFiles* wf : watched ) {
+		if ( wf ) {
+			wf->watcher = nullptr;
+		}
+	}
+}
+
+void T_FilesWatcher::check( )
+{
+	for ( T_WatchedFiles* wf : watched ) {
+		if ( wf ) {
+			wf->triggered = false;
+		}
+	}
+
+	inotify_event ie;
+	while ( read( fd , &ie , sizeof( ie ) ) == sizeof( ie ) ) {
+		if ( ( ie.mask & ( IN_CLOSE_WRITE | IN_DELETE_SELF ) ) == 0 ) {
+			continue;
+		}
+
+		for ( T_WatchedFiles* wf : watched ) {
+			if ( !wf || wf->triggered ) {
+				continue;
+			}
+			auto const& idl( wf->identifiers );
+			if ( find( idl , ie.wd ) != idl.end( ) ) {
+				wf->triggered = true;
+				wf->callback( );
+			}
+		}
+	}
+}
+
+/*= T_WatchedFiles ===========================================================*/
+
+T_WatchedFiles::T_WatchedFiles( T_WatchedFiles&& other ) noexcept
+	: watcher( other.watcher ) , callback( other.callback ) ,
+		triggered( other.triggered ) ,
+		identifiers( std::move( other.identifiers ) )
+{
+	if ( watcher ) {
+		other.watcher = nullptr;
+		*( find( watcher->watched , &other ) ) = this;
+	}
+}
+
+T_WatchedFiles::T_WatchedFiles(
+		__rw__	T_FilesWatcher& watcher ,
+		__rd__	const F_OnFileChanges callback )
+	: watcher( &watcher ) , callback( callback ) , triggered( false )
+{
+	watcher.watched.push_back( this );
+}
+
+T_WatchedFiles::~T_WatchedFiles( )
+{
+	clear( );
+	if ( watcher ) {
+		watcher->watched.erase( find( watcher->watched , this ) );
+	}
+}
+
+void T_WatchedFiles::clear( )
+{
+	if ( watcher ) {
+		const auto fd( watcher->fd );
+		for ( int wd : identifiers ) {
+			inotify_rm_watch( fd , wd );
+		}
+	}
+	identifiers.clear( );
+}
+
+bool T_WatchedFiles::watch(
+		__rd__	std::string const& file )
+{
+	static constexpr auto inFlags( IN_CLOSE_WRITE | IN_DELETE_SELF );
+	if ( watcher ) {
+		const auto wd( inotify_add_watch( watcher->fd ,
+					file.c_str( ) , inFlags ) );
+		if ( wd == -1 ) {
+			return false;
+		}
+		if ( find( identifiers , wd ) == identifiers.end( ) ) {
+			identifiers.push_back( wd );
+		}
+		return true;
+	}
+	return false;
+}
+
+
+/*= T_ShaderCode =============================================================*/
+
+T_ShaderCode::T_ShaderCode(
+		__rd__	const int nparts )
+	: code( nparts , nullptr )
+{ }
+
+T_ShaderCode::~T_ShaderCode( )
+{
+	for ( char* str : code ) {
+		delete[] str;
+	}
+}
+
+/*----------------------------------------------------------------------------*/
+
+void T_ShaderCode::setPart(
+			__rd__	const int index ,
+			__rd__	char const* const string )
+{
+	assert( code[ index ] == nullptr );
+
+	const int len( strlen( string ) + 1 );
+	char buffer[ 32 ];
+	const int extraLen( index == 0 ? 0
+			: snprintf( buffer , sizeof( buffer ) ,
+				"\n#line 0 %d\n" , index ) );
+
+	char* const output( new char[ extraLen + len ] );
+	if ( index != 0 ) {
+		memcpy( output , buffer , extraLen );
+	}
+	strcpy( output + extraLen , string );
+	code[ index ] = output;
+}
+
+void T_ShaderCode::setPart(
+		__rd__	const int index ,
+		__rd__	void const* const data ,
+		__rd__	const int size )
+{
+	assert( code[ index ] == nullptr );
+
+	char buffer[ 32 ];
+	const int extraLen( index == 0 ? 0
+			: snprintf( buffer , sizeof( buffer ) ,
+				"\n#line 0 %d\n" , index ) );
+
+	char* const output( new char[ extraLen + size + 1 ] );
+	if ( index != 0 ) {
+		memcpy( output , buffer , extraLen );
+	}
+	memcpy( output + extraLen , data , size );
+	output[ extraLen + size ] = 0;
+	code[ index ] = output;
+}
+
+bool T_ShaderCode::loadPart(
+		__rd__	const int index ,
+		__rd__	std::string const& source ,
+		__rw__	std::vector< std::string >& errors )
+{
+	assert( code[ index ] == nullptr );
+
+	FILE * f = fopen( source.c_str( ) , "r" );
+	if ( !f ) {
+		std::string error( "File not found: " );
+		error += source;
+		errors.push_back( error );
+		return false;
+	}
+
+	char buffer[ 32 ];
+	const int extraLen( index == 0 ? 0
+			: snprintf( buffer , sizeof( buffer ) ,
+				"\n#line 0 %d\n" , index ) );
+
+	fseek( f , 0 , SEEK_END );
+	const size_t size( ftell( f ) );
+	fseek( f , 0 , SEEK_SET );
+
+	char* const output( new char[ extraLen + size + 1 ] );
+	if ( index != 0 ) {
+		memcpy( output , buffer , extraLen );
+	}
+	if ( fread( output + extraLen , 1 , size , f ) != size ) {
+		fclose( f );
+		delete[] output;
+		std::string error( "Could not read file: " );
+		error += source;
+		errors.push_back( error );
+		return false;
+	}
+	output[ extraLen + size ] = 0;
+	fclose( f );
+	code[ index ] = output;
+	return true;
+}
+
+/*----------------------------------------------------------------------------*/
+
+GLuint T_ShaderCode::createProgram(
+		__rd__	GLenum type ,
+		__rw__	std::vector< std::string >& errors ) const
+{
+	GLenum sid = glCreateShaderProgramv( type , code.size( ) , &code[ 0 ] );
+	if ( sid == 0 ) {
+		errors.push_back( "Failed to create GL program" );
+		return sid;
+	}
+
+	int infoLogLength;
+	glGetProgramiv( sid , GL_INFO_LOG_LENGTH , &infoLogLength );
+	if ( infoLogLength ) {
+		char buffer[ infoLogLength + 1 ];
+		glGetProgramInfoLog( sid , infoLogLength , nullptr , buffer );
+		char* start( buffer );
+		char* found( strchr( buffer , '\n' ) );
+		while ( found ) {
+			*found = 0;
+			errors.push_back( start );
+			start = found + 1;
+			found = strchr( start , '\n' );
+		}
+		if ( start < &buffer[ infoLogLength - 1 ] ) {
+			errors.push_back( start );
+		}
+	}
+
+	int lnk;
+	glGetProgramiv( sid , GL_LINK_STATUS , &lnk );
+	if ( !lnk ) {
+		glDeleteProgram( sid );
+		return 0;
+	}
+
+	return sid;
+}
+
+
+/*= T_Camera =================================================================*/
+
+void T_Camera::handleDND(
+		__rd__	ImVec2 const& move ,
+		__rd__	const bool hasCtrl ,
+		__rd__	const bool hasShift ,
+		__rd__	const bool lmb		// Left mouse button
+	)
+{
+	if ( move.x == 0 || move.y == 0 ) {
+		return;
+	}
+
+	const float fdx( move.x * .1f * ( hasCtrl ? 1.f : .1f ) );
+	const float fdy( move.y * .1f * ( hasCtrl ? 1.f : .1f ) );
+
+	if ( lmb && hasShift ) {
+		// Left mouse button, shift - move camera
+		const auto side( glm::normalize( glm::cross( up , dir ) ) );
+		lookAt += .01f * ( side * fdx + up * fdy );
+	} else if ( lmb ) {
+		// Left mouse button, no shift - change yaw/pitch
+		updateAngle( angles.y , fdx );
+		updateAngle( angles.x , fdy );
+	} else {
+		// Right mouse button - change roll
+		updateAngle( angles.z , fdx );
+	}
+	update( );
+}
+
+void T_Camera::handleWheel(
+		__rd__	const float wheel ,
+		__rd__	const bool hasCtrl ,
+		__rd__	const bool hasShift
+	)
+{
+	const float delta( wheel * ( hasCtrl ? 1.f : .1f) );
+	if ( hasShift ) {
+		fov = std::max( 1.f , std::min( 179.f , fov + delta ) );
+	} else {
+		distance = std::max( .01f , distance - delta );
+	}
+	update( );
+}
+
+/*----------------------------------------------------------------------------*/
+
+void T_Camera::makeUI( )
+{
+	if ( !ImGui::CollapsingHeader( "Camera" ) ) {
+		return;
+	}
+
+	const bool changed[] = {
+		ImGui::DragFloat3( "Look at" , &lookAt.x ) ,
+		ImGui::DragFloat( "Distance" , &distance , .1f ,
+				.1f , 1e8 , "%.1f" ) ,
+		ImGui::DragFloat3( "Angles" , &angles.x , .01f , -180 , 180 ) ,
+		ImGui::DragFloat( "FoV" , &fov , .01f , .01f , 179.9f )
+	};
+
+	for ( unsigned i = 0 ; i < sizeof( changed ) / sizeof( bool ) ; i ++ ) {
+		if ( changed[ i ] ) {
+			update( );
+			break;
+		}
+	}
+}
+
+/*----------------------------------------------------------------------------*/
+
+void T_Camera::update( )
+{
+	anglesToMatrix( &angles.x , &rotMat[ 0 ].x );
+	dir = glm::vec3( 0 , 0 , -distance ) * rotMat;
+	up = glm::vec3( 0 , 1 , 0 ) * rotMat;
+	pos = lookAt - dir;
+	np = 2 * tan( M_PI * ( 180. - fov ) / 360. );
+}
diff --git a/utilities.hh b/utilities.hh
new file mode 100644
index 0000000..b1d8f7e
--- /dev/null
+++ b/utilities.hh
@@ -0,0 +1,166 @@
+#pragma once
+#ifndef REAL_BUILD
+# include "externals.hh"
+#endif
+
+/*= Utilities ================================================================*/
+
+// Disable next ImGui button(s)
+void disableButton( );
+// Re-enable ImGui buttons
+inline void reenableButtons( )
+{
+	ImGui::PopStyleColor( 3 );
+}
+
+/*----------------------------------------------------------------------------*/
+
+// Add some value to an angle, keeping it in [-180;180]
+void updateAngle(
+		__rw__	float& initial ,
+		__rd__	const float delta
+	);
+// Make a rotation matrix from three YPR angles (in degrees)
+void anglesToMatrix(
+		__rd__	float const* angles ,
+		__wr__	float* matrix );
+
+/*----------------------------------------------------------------------------*/
+
+// Helpers for finding entries in collections
+template< typename T , typename I >
+inline auto find(
+		__rd__ T const& collection ,
+		__rd__ I const& item )
+{
+	return std::find( collection.begin( ) , collection.end( ) , item );
+}
+
+template< typename T , typename I >
+inline auto find(
+		__rw__ T& collection ,
+		__rd__ I const& item )
+{
+	return std::find( collection.begin( ) , collection.end( ) , item );
+}
+
+
+/*= T_FilesWatcher / T_WatchedFiles ==========================================*/
+
+struct T_FilesWatcher;
+struct T_WatchedFiles;
+using F_OnFileChanges = std::function< void( void ) >;
+
+struct T_FilesWatcher
+{
+	friend struct T_WatchedFiles;
+
+	T_FilesWatcher( T_FilesWatcher const& ) = delete;
+
+	T_FilesWatcher( );
+	T_FilesWatcher( T_FilesWatcher&& ) noexcept;
+	~T_FilesWatcher( );
+
+	void check( );
+
+    private:
+	int fd;
+	std::vector< T_WatchedFiles* > watched;
+};
+
+/*----------------------------------------------------------------------------*/
+
+struct T_WatchedFiles
+{
+	friend struct T_FilesWatcher;
+
+	T_WatchedFiles( ) = delete;
+	T_WatchedFiles( T_WatchedFiles const& ) = delete;
+
+	T_WatchedFiles( T_WatchedFiles&& ) noexcept;
+	T_WatchedFiles(
+			__rw__ T_FilesWatcher& watcher ,
+			__rd__ const F_OnFileChanges callback );
+
+	~T_WatchedFiles( );
+
+	void clear( );
+	bool watch( __rd__ std::string const& file );
+
+    private:
+	T_FilesWatcher* watcher;
+	const F_OnFileChanges callback;
+	bool triggered;
+	std::vector< int > identifiers;
+};
+
+
+/*= T_ShaderCode =============================================================*/
+
+struct T_ShaderCode
+{
+	T_ShaderCode( ) = delete;
+	T_ShaderCode( T_ShaderCode const& ) = delete;
+	T_ShaderCode( T_ShaderCode&& ) = delete;
+
+	explicit T_ShaderCode( __rd__ const int nparts );
+	~T_ShaderCode( );
+
+	void setPart(
+			__rd__	const int index ,
+			__rd__	char const* const string );
+	void setPart(
+			__rd__	const int index ,
+			__rd__	void const* const data ,
+			__rd__	const int size );
+	bool loadPart(
+			__rd__	const int index ,
+			__rd__	std::string const& source ,
+			__rw__	std::vector< std::string >& errors );
+
+	GLuint createProgram(
+			__rd__	GLenum type ,
+			__rw__	std::vector< std::string >& errors ) const;
+
+    private:
+	std::vector< char* > code;
+};
+
+
+/*= T_Camera =================================================================*/
+
+struct T_Camera
+{
+	glm::vec3 lookAt;
+	glm::vec3 angles;
+	float distance = 10;
+	float fov = 90;
+
+	// Everything below is updated by update()
+	glm::vec3 dir;
+	glm::vec3 up;
+	glm::vec3 pos;
+	float np;
+
+	T_Camera()
+		{ update( ); }
+
+	void handleDND(
+			__rd__	ImVec2 const& move ,
+			__rd__	const bool hasCtrl ,
+			__rd__	const bool hasShift ,
+			__rd__	const bool lmb		// Left mouse button
+		);
+	void handleWheel(
+			__rd__	const float wheel ,
+			__rd__	const bool hasCtrl ,
+			__rd__	const bool hasShift
+		);
+
+	void makeUI( );
+
+    private:
+	glm::mat3x3 rotMat;
+	float rotationMatrix[ 9 ];
+	void update( );
+};