diff --git a/CMakeLists.txt b/CMakeLists.txt
index f62574dd47032d6ce9c91112fff13e16fcce88c6..bc2b9c25edaac17a99e238496743d50fdd3aa684 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -103,6 +103,12 @@ message(">> lava::file")
 add_library(lava.file STATIC
         ${LIBLAVA_DIR}/file/file.cpp
         ${LIBLAVA_DIR}/file/file.hpp
+        ${LIBLAVA_DIR}/file/file_system.cpp
+        ${LIBLAVA_DIR}/file/file_system.hpp
+        ${LIBLAVA_DIR}/file/file_utils.cpp
+        ${LIBLAVA_DIR}/file/file_utils.hpp
+        ${LIBLAVA_DIR}/file/json_file.cpp
+        ${LIBLAVA_DIR}/file/json_file.hpp
         )
 
 target_include_directories(lava.file PUBLIC
diff --git a/DOCS.md b/DOCS.md
index 45021da90ca65e6a656226a4ba737cd812cf9185..67dc6cffdf6f3a0919d478e7d5e6f447de58a695 100644
--- a/DOCS.md
+++ b/DOCS.md
@@ -353,7 +353,7 @@ int main(int argc, char* argv[]) {
 
 #### lava [file](https://github.com/liblava/liblava/tree/master/liblava/file)
 
-[![file](https://img.shields.io/badge/lava-file-blue.svg)](https://github.com/liblava/liblava/tree/master/liblava/file/file.hpp)
+[![file](https://img.shields.io/badge/lava-file-blue.svg)](https://github.com/liblava/liblava/tree/master/liblava/file/file.hpp) [![file_system](https://img.shields.io/badge/lava-file_system-blue.svg)](https://github.com/liblava/liblava/tree/master/liblava/file/file_system.hpp) [![file_utils](https://img.shields.io/badge/lava-file_utils-blue.svg)](https://github.com/liblava/liblava/tree/master/liblava/file/file_utils.hpp) [![json_file](https://img.shields.io/badge/lava-json_file-blue.svg)](https://github.com/liblava/liblava/tree/master/liblava/file/json_file.hpp)
 
 #### lava [util](https://github.com/liblava/liblava/tree/master/liblava/util)
 
diff --git a/liblava/app/app.hpp b/liblava/app/app.hpp
index 58d690a2864be68ff6512e90e437e1fa3e8921a4..201458d42285bbab5ef012b2d5f07218f62965db 100644
--- a/liblava/app/app.hpp
+++ b/liblava/app/app.hpp
@@ -100,7 +100,7 @@ namespace lava {
         bool toggle_v_sync = false;
         ui32 frame_counter = 0;
 
-        file_callback config_callback;
+        json_file::callback config_callback;
 
         id block_command;
     };
diff --git a/liblava/app/gui.hpp b/liblava/app/gui.hpp
index 7471e55d10a994a3e8bda1111efb1ca61fcdb4a6..78cfeaf07b89a802e3b5c40829d7368919e266ee 100644
--- a/liblava/app/gui.hpp
+++ b/liblava/app/gui.hpp
@@ -5,6 +5,7 @@
 #pragma once
 
 #include <liblava/block/pipeline.hpp>
+#include <liblava/file.hpp>
 #include <liblava/frame/input.hpp>
 #include <liblava/resource/texture.hpp>
 
diff --git a/liblava/file.hpp b/liblava/file.hpp
index 510bd070f63cdb1db7009fb4f43879d576c5e737..c99d3432a67d59c8063a56d24954e80068e9bf54 100644
--- a/liblava/file.hpp
+++ b/liblava/file.hpp
@@ -5,3 +5,6 @@
 #pragma once
 
 #include <liblava/file/file.hpp>
+#include <liblava/file/file_system.hpp>
+#include <liblava/file/file_utils.hpp>
+#include <liblava/file/json_file.hpp>
diff --git a/liblava/file/file.cpp b/liblava/file/file.cpp
index 783ff149523317bede6e2bba9abbb5f7f1659b07..4193c0f6b6dad10b0235d1e7f1ad0d5d45e71df7 100644
--- a/liblava/file/file.cpp
+++ b/liblava/file/file.cpp
@@ -4,201 +4,9 @@
 
 #include <physfs.h>
 #include <liblava/file/file.hpp>
-#include <liblava/util/utility.hpp>
-
-bool lava::read_file(std::vector<char>& out, name filename) {
-    std::ifstream file(filename, std::ios::ate | std::ios::binary);
-    assert(file.is_open());
-
-    if (!file.is_open())
-        return false;
-
-    out.clear();
-
-    auto file_size = to_size_t(file.tellg());
-    if (file_size > 0) {
-        out.resize(file_size);
-        file.seekg(0, std::ios::beg);
-        file.read(out.data(), file_size);
-    }
-
-    file.close();
-    return true;
-}
-
-bool lava::write_file(name filename, char const* data, size_t data_size) {
-    std::ofstream file(filename, std::ofstream::binary);
-    assert(file.is_open());
-
-    if (!file.is_open())
-        return false;
-
-    file.write(data, data_size);
-    file.close();
-    return true;
-}
-
-bool lava::extension(name file_name, name extension) {
-    string fn = file_name;
-    string ext = extension;
-
-    string to_check = fn.substr(fn.find_last_of('.') + 1);
-
-    std::transform(to_check.begin(), to_check.end(), to_check.begin(), ::tolower);
-    std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
-
-    return to_check == ext;
-}
-
-bool lava::extension(name filename, names extensions) {
-    for (auto& ex : extensions)
-        if (extension(filename, ex))
-            return true;
-
-    return false;
-}
-
-lava::string lava::get_filename_from(string_ref path, bool with_extension) {
-    fs::path target(path);
-    return with_extension ? target.filename().string() : target.stem().string();
-}
-
-bool lava::remove_existing_path(string& target, string_ref path) {
-    auto pos = target.find(path);
-    if (pos != std::string::npos) {
-        target.erase(pos, path.length());
-
-#ifdef _WIN32
-        std::replace(target.begin(), target.end(), '\\', '/');
-#endif
-
-        return true;
-    }
-
-    return false;
-}
 
 namespace lava {
 
-    internal_version file_system::get_version() {
-        PHYSFS_Version result;
-        PHYSFS_getLinkedVersion(&result);
-
-        return { result.major, result.minor, result.patch };
-    }
-
-    name file_system::get_base_dir() {
-        return PHYSFS_getBaseDir();
-    }
-
-    string file_system::get_base_dir_str() {
-        return string(get_base_dir());
-    }
-
-    name file_system::get_pref_dir() {
-        return PHYSFS_getPrefDir(instance().org, instance().app);
-    }
-
-    string file_system::get_res_dir() {
-        string res_dir = get_base_dir();
-        res_dir += instance().res_path;
-        string_ref const_res_dir = res_dir;
-
-        return fs::path(const_res_dir).lexically_normal().string();
-    }
-
-    bool file_system::mount(string_ref path) {
-        return PHYSFS_mount(str(path), nullptr, 1) != 0;
-    }
-
-    bool file_system::mount(name base_dir_path) {
-        return mount(get_base_dir_str() + base_dir_path);
-    }
-
-    bool file_system::exists(name file) {
-        return PHYSFS_exists(file) != 0;
-    }
-
-    name file_system::get_real_dir(name file) {
-        return PHYSFS_getRealDir(file);
-    }
-
-    string_list file_system::enumerate_files(name path) {
-        string_list result;
-
-        auto rc = PHYSFS_enumerateFiles(path);
-        for (auto i = rc; *i != nullptr; ++i)
-            result.push_back(*i);
-
-        PHYSFS_freeList(rc);
-
-        return result;
-    }
-
-    bool file_system::initialize(name argv_0, name o, name a, name e) {
-        assert(!initialized); // only once
-        if (initialized)
-            return initialized;
-
-        if (!initialized) {
-            PHYSFS_init(argv_0);
-
-            PHYSFS_setSaneConfig(o, a, e, 0, 0);
-            initialized = true;
-
-            org = o;
-            app = a;
-            ext = e;
-        }
-
-        return initialized;
-    }
-
-    void file_system::terminate() {
-        if (!initialized)
-            return;
-
-        PHYSFS_deinit();
-    }
-
-    void file_system::mount_res() {
-#if LIBLAVA_DEBUG
-#    if _WIN32
-        res_path = "../../res/";
-#    else
-        res_path = "../res/";
-#    endif
-#else
-        res_path = "res/";
-#endif
-
-        if (fs::exists(str(get_res_dir())))
-            if (file_system::mount(str(res_path)))
-                log()->debug("mount {}", str(get_res_dir()));
-
-        auto cwd_res_dir = fs::current_path().append("res/").lexically_normal().string();
-
-        if (fs::exists(cwd_res_dir) && (cwd_res_dir != get_res_dir()))
-            if (file_system::mount(cwd_res_dir))
-                log()->debug("mount {}", str(cwd_res_dir));
-
-        string archive_file = "res.zip";
-        if (fs::exists({ archive_file }))
-            if (file_system::mount(str(archive_file)))
-                log()->debug("mount {}", str(archive_file));
-    }
-
-    bool file_system::create_data_folder() {
-        fs::path data_path = fs::current_path();
-        data_path += fs::path::preferred_separator;
-        data_path += "data";
-
-        if (!fs::exists(data_path))
-            fs::create_directories(data_path);
-
-        return fs::exists(data_path);
-    }
-
     file::file(name p, bool write) {
         open(p, write);
     }
@@ -305,64 +113,4 @@ namespace lava {
         return file_error_result;
     }
 
-    json_file::json_file(name path)
-    : path(path) {}
-
-    void json_file::add(file_callback* callback) {
-        callbacks.push_back(callback);
-    }
-
-    void json_file::remove(file_callback* callback) {
-        lava::remove(callbacks, callback);
-    }
-
-    bool json_file::load() {
-        scope_data data;
-        if (!load_file_data(path, data))
-            return false;
-
-        auto j = json::parse(data.ptr, data.ptr + data.size);
-
-        for (auto callback : callbacks)
-            callback->on_load(j);
-
-        return true;
-    }
-
-    bool json_file::save() {
-        file file(str(path), true);
-        if (!file.opened()) {
-            log()->error("save file {}", str(path));
-            return false;
-        }
-
-        json j;
-
-        for (auto callback : callbacks)
-            callback->on_save(j);
-
-        auto jString = j.dump(4);
-
-        file.write(jString.data(), jString.size());
-
-        return true;
-    }
-
 } // namespace lava
-
-bool lava::load_file_data(string_ref filename, data& target) {
-    file file(str(filename));
-    if (!file.opened())
-        return false;
-
-    target.set(to_size_t(file.get_size()));
-    if (!target.ptr)
-        return false;
-
-    if (file_error(file.read(target.ptr))) {
-        log()->error("read file {}", filename);
-        return false;
-    }
-
-    return true;
-}
diff --git a/liblava/file/file.hpp b/liblava/file/file.hpp
index 3376b510ecf954222676d5f8d6cb704575042251..00d33e927cf967086c8e8c2373564cd910fa1e4f 100644
--- a/liblava/file/file.hpp
+++ b/liblava/file/file.hpp
@@ -4,102 +4,14 @@
 
 #pragma once
 
-#include <filesystem>
 #include <fstream>
 #include <liblava/core/data.hpp>
-#include <liblava/util/log.hpp>
-#include <nlohmann/json.hpp>
 
 // fwd
 struct PHYSFS_File;
 
 namespace lava {
 
-    constexpr name _zip_ = "zip";
-    constexpr name _config_file_ = "config.json";
-
-    using json = nlohmann::json;
-
-    namespace fs = std::filesystem;
-
-    bool read_file(std::vector<char>& out, name filename);
-
-    bool write_file(name filename, char const* data, size_t data_size);
-
-    bool extension(name file_name, name extension);
-
-    bool extension(name filename, names extensions);
-
-    string get_filename_from(string_ref path, bool with_extension = false);
-
-    bool remove_existing_path(string& target, string_ref path);
-
-    struct file_guard : no_copy_no_move {
-        explicit file_guard(name filename = "")
-        : filename(filename) {}
-        explicit file_guard(string filename)
-        : filename(filename) {}
-
-        ~file_guard() {
-            if (remove)
-                fs::remove(filename);
-        }
-
-        string filename;
-        bool remove = true;
-    };
-
-    struct file_system : no_copy_no_move {
-        static file_system& instance() {
-            static file_system fs;
-            return fs;
-        }
-
-        static internal_version get_version();
-
-        static name get_base_dir();
-        static string get_base_dir_str();
-        static name get_pref_dir();
-        static string get_res_dir();
-
-        static bool mount(string_ref path);
-        static bool mount(name base_dir_path);
-        static bool exists(name file);
-        static name get_real_dir(name file);
-        static string_list enumerate_files(name path);
-
-        bool initialize(name argv_0, name org, name app, name ext);
-        void terminate();
-
-        void mount_res();
-        bool create_data_folder();
-
-        name get_org() const {
-            return org;
-        }
-        name get_app() const {
-            return app;
-        }
-        name get_ext() const {
-            return ext;
-        }
-
-        bool ready() const {
-            return initialized;
-        }
-
-    private:
-        file_system() = default;
-
-        bool initialized = false;
-
-        name org = nullptr;
-        name app = nullptr;
-        name ext = nullptr;
-
-        string res_path;
-    };
-
     enum class file_type : type {
         none = 0,
         fs,
@@ -151,41 +63,4 @@ namespace lava {
         mutable std::ofstream o_stream;
     };
 
-    bool load_file_data(string_ref filename, data& target);
-
-    struct file_data : scope_data {
-        explicit file_data(string_ref filename) {
-            load_file_data(filename, *this);
-        }
-    };
-
-    struct file_callback {
-        using list = std::vector<file_callback*>;
-
-        using func = std::function<void(json&)>;
-        func on_load;
-        func on_save;
-    };
-
-    struct json_file {
-        explicit json_file(name path = _config_file_);
-
-        void add(file_callback* callback);
-        void remove(file_callback* callback);
-
-        void set(name value) {
-            path = value;
-        }
-        name get() const {
-            return str(path);
-        }
-
-        bool load();
-        bool save();
-
-    private:
-        string path;
-        file_callback::list callbacks;
-    };
-
 } // namespace lava
diff --git a/liblava/file/file_system.cpp b/liblava/file/file_system.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c6605f535a184209f705d2bac1181a4d948b3e53
--- /dev/null
+++ b/liblava/file/file_system.cpp
@@ -0,0 +1,130 @@
+// file      : liblava/file/file_system.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <physfs.h>
+#include <liblava/file/file_system.hpp>
+#include <liblava/util/log.hpp>
+
+namespace lava {
+
+    internal_version file_system::get_version() {
+        PHYSFS_Version result;
+        PHYSFS_getLinkedVersion(&result);
+
+        return { result.major, result.minor, result.patch };
+    }
+
+    name file_system::get_base_dir() {
+        return PHYSFS_getBaseDir();
+    }
+
+    string file_system::get_base_dir_str() {
+        return string(get_base_dir());
+    }
+
+    name file_system::get_pref_dir() {
+        return PHYSFS_getPrefDir(instance().org, instance().app);
+    }
+
+    string file_system::get_res_dir() {
+        string res_dir = get_base_dir();
+        res_dir += instance().res_path;
+        string_ref const_res_dir = res_dir;
+
+        return fs::path(const_res_dir).lexically_normal().string();
+    }
+
+    bool file_system::mount(string_ref path) {
+        return PHYSFS_mount(str(path), nullptr, 1) != 0;
+    }
+
+    bool file_system::mount(name base_dir_path) {
+        return mount(get_base_dir_str() + base_dir_path);
+    }
+
+    bool file_system::exists(name file) {
+        return PHYSFS_exists(file) != 0;
+    }
+
+    name file_system::get_real_dir(name file) {
+        return PHYSFS_getRealDir(file);
+    }
+
+    string_list file_system::enumerate_files(name path) {
+        string_list result;
+
+        auto rc = PHYSFS_enumerateFiles(path);
+        for (auto i = rc; *i != nullptr; ++i)
+            result.push_back(*i);
+
+        PHYSFS_freeList(rc);
+
+        return result;
+    }
+
+    bool file_system::initialize(name argv_0, name o, name a, name e) {
+        assert(!initialized); // only once
+        if (initialized)
+            return initialized;
+
+        if (!initialized) {
+            PHYSFS_init(argv_0);
+
+            PHYSFS_setSaneConfig(o, a, e, 0, 0);
+            initialized = true;
+
+            org = o;
+            app = a;
+            ext = e;
+        }
+
+        return initialized;
+    }
+
+    void file_system::terminate() {
+        if (!initialized)
+            return;
+
+        PHYSFS_deinit();
+    }
+
+    void file_system::mount_res() {
+#if LIBLAVA_DEBUG
+#    if _WIN32
+        res_path = "../../res/";
+#    else
+        res_path = "../res/";
+#    endif
+#else
+        res_path = "res/";
+#endif
+
+        if (fs::exists(str(get_res_dir())))
+            if (file_system::mount(str(res_path)))
+                log()->debug("mount {}", str(get_res_dir()));
+
+        auto cwd_res_dir = fs::current_path().append("res/").lexically_normal().string();
+
+        if (fs::exists(cwd_res_dir) && (cwd_res_dir != get_res_dir()))
+            if (file_system::mount(cwd_res_dir))
+                log()->debug("mount {}", str(cwd_res_dir));
+
+        string archive_file = "res.zip";
+        if (fs::exists({ archive_file }))
+            if (file_system::mount(str(archive_file)))
+                log()->debug("mount {}", str(archive_file));
+    }
+
+    bool file_system::create_data_folder() {
+        fs::path data_path = fs::current_path();
+        data_path += fs::path::preferred_separator;
+        data_path += "data";
+
+        if (!fs::exists(data_path))
+            fs::create_directories(data_path);
+
+        return fs::exists(data_path);
+    }
+
+} // namespace lava
diff --git a/liblava/file/file_system.hpp b/liblava/file/file_system.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..89cfb80606f1282f1e7127f7a832f820041fa32d
--- /dev/null
+++ b/liblava/file/file_system.hpp
@@ -0,0 +1,67 @@
+// file      : liblava/file/file_system.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <filesystem>
+#include <liblava/core/version.hpp>
+
+namespace lava {
+
+    constexpr name _zip_ = "zip";
+
+    namespace fs = std::filesystem;
+
+    struct file_system : no_copy_no_move {
+        static file_system& instance() {
+            static file_system fs;
+            return fs;
+        }
+
+        static internal_version get_version();
+
+        static name get_base_dir();
+        static string get_base_dir_str();
+        static name get_pref_dir();
+        static string get_res_dir();
+
+        static bool mount(string_ref path);
+        static bool mount(name base_dir_path);
+        static bool exists(name file);
+        static name get_real_dir(name file);
+        static string_list enumerate_files(name path);
+
+        bool initialize(name argv_0, name org, name app, name ext);
+        void terminate();
+
+        void mount_res();
+        bool create_data_folder();
+
+        name get_org() const {
+            return org;
+        }
+        name get_app() const {
+            return app;
+        }
+        name get_ext() const {
+            return ext;
+        }
+
+        bool ready() const {
+            return initialized;
+        }
+
+    private:
+        file_system() = default;
+
+        bool initialized = false;
+
+        name org = nullptr;
+        name app = nullptr;
+        name ext = nullptr;
+
+        string res_path;
+    };
+
+} // namespace lava
diff --git a/liblava/file/file_utils.cpp b/liblava/file/file_utils.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1598d784c1a6dc9a016316d61d59ed4e318c5d60
--- /dev/null
+++ b/liblava/file/file_utils.cpp
@@ -0,0 +1,106 @@
+// file      : liblava/file/file_utils.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/file/file.hpp>
+#include <liblava/file/file_system.hpp>
+#include <liblava/file/file_utils.hpp>
+#include <liblava/util/log.hpp>
+
+bool lava::read_file(std::vector<char>& out, name filename) {
+    std::ifstream file(filename, std::ios::ate | std::ios::binary);
+    assert(file.is_open());
+
+    if (!file.is_open())
+        return false;
+
+    out.clear();
+
+    auto file_size = to_size_t(file.tellg());
+    if (file_size > 0) {
+        out.resize(file_size);
+        file.seekg(0, std::ios::beg);
+        file.read(out.data(), file_size);
+    }
+
+    file.close();
+    return true;
+}
+
+bool lava::write_file(name filename, char const* data, size_t data_size) {
+    std::ofstream file(filename, std::ofstream::binary);
+    assert(file.is_open());
+
+    if (!file.is_open())
+        return false;
+
+    file.write(data, data_size);
+    file.close();
+    return true;
+}
+
+bool lava::extension(name file_name, name extension) {
+    string fn = file_name;
+    string ext = extension;
+
+    string to_check = fn.substr(fn.find_last_of('.') + 1);
+
+    std::transform(to_check.begin(), to_check.end(), to_check.begin(), ::tolower);
+    std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
+
+    return to_check == ext;
+}
+
+bool lava::extension(name filename, names extensions) {
+    for (auto& ex : extensions)
+        if (extension(filename, ex))
+            return true;
+
+    return false;
+}
+
+lava::string lava::get_filename_from(string_ref path, bool with_extension) {
+    fs::path target(path);
+    return with_extension ? target.filename().string() : target.stem().string();
+}
+
+bool lava::remove_existing_path(string& target, string_ref path) {
+    auto pos = target.find(path);
+    if (pos != std::string::npos) {
+        target.erase(pos, path.length());
+
+#ifdef _WIN32
+        std::replace(target.begin(), target.end(), '\\', '/');
+#endif
+
+        return true;
+    }
+
+    return false;
+}
+
+bool lava::load_file_data(string_ref filename, data& target) {
+    file file(str(filename));
+    if (!file.opened())
+        return false;
+
+    target.set(to_size_t(file.get_size()));
+    if (!target.ptr)
+        return false;
+
+    if (file_error(file.read(target.ptr))) {
+        log()->error("read file {}", filename);
+        return false;
+    }
+
+    return true;
+}
+
+namespace lava {
+
+    file_remover::~file_remover() {
+        if (remove)
+            fs::remove(filename);
+    }
+
+} // namespace lava
diff --git a/liblava/file/file_utils.hpp b/liblava/file/file_utils.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..9717cd1294a1091b294b81ad51c955d4bacca9e9
--- /dev/null
+++ b/liblava/file/file_utils.hpp
@@ -0,0 +1,43 @@
+// file      : liblava/file/file_utils.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/core/data.hpp>
+
+namespace lava {
+
+    bool read_file(std::vector<char>& out, name filename);
+
+    bool write_file(name filename, char const* data, size_t data_size);
+
+    bool extension(name file_name, name extension);
+
+    bool extension(name filename, names extensions);
+
+    string get_filename_from(string_ref path, bool with_extension = false);
+
+    bool remove_existing_path(string& target, string_ref path);
+
+    bool load_file_data(string_ref filename, data& target);
+
+    struct file_data : scope_data {
+        explicit file_data(string_ref filename) {
+            load_file_data(filename, *this);
+        }
+    };
+
+    struct file_remover : no_copy_no_move {
+        explicit file_remover(name filename = "")
+        : filename(filename) {}
+        explicit file_remover(string filename)
+        : filename(filename) {}
+
+        ~file_remover();
+
+        string filename;
+        bool remove = true;
+    };
+
+} // namespace lava
diff --git a/liblava/file/json_file.cpp b/liblava/file/json_file.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..db26b07e8ebae958117b7d17fb09681d4fde9f25
--- /dev/null
+++ b/liblava/file/json_file.cpp
@@ -0,0 +1,56 @@
+// file      : liblava/file/json_file.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/file/file.hpp>
+#include <liblava/file/file_utils.hpp>
+#include <liblava/file/json_file.hpp>
+#include <liblava/util/log.hpp>
+#include <liblava/util/utility.hpp>
+
+namespace lava {
+
+    json_file::json_file(name path)
+    : path(path) {}
+
+    void json_file::add(callback* callback) {
+        callbacks.push_back(callback);
+    }
+
+    void json_file::remove(callback* callback) {
+        lava::remove(callbacks, callback);
+    }
+
+    bool json_file::load() {
+        scope_data data;
+        if (!load_file_data(path, data))
+            return false;
+
+        auto j = json::parse(data.ptr, data.ptr + data.size);
+
+        for (auto callback : callbacks)
+            callback->on_load(j);
+
+        return true;
+    }
+
+    bool json_file::save() {
+        file file(str(path), true);
+        if (!file.opened()) {
+            log()->error("save file {}", str(path));
+            return false;
+        }
+
+        json j;
+
+        for (auto callback : callbacks)
+            callback->on_save(j);
+
+        auto jString = j.dump(4);
+
+        file.write(jString.data(), jString.size());
+
+        return true;
+    }
+
+} // namespace lava
diff --git a/liblava/file/json_file.hpp b/liblava/file/json_file.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d5e1533d22644795711478ed355ff050777d38b1
--- /dev/null
+++ b/liblava/file/json_file.hpp
@@ -0,0 +1,45 @@
+// file      : liblava/file/json_file.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/core/types.hpp>
+#include <nlohmann/json.hpp>
+
+namespace lava {
+
+    constexpr name _config_file_ = "config.json";
+
+    using json = nlohmann::json;
+
+    struct json_file {
+        explicit json_file(name path = _config_file_);
+
+        struct callback {
+            using list = std::vector<callback*>;
+
+            using func = std::function<void(json&)>;
+            func on_load;
+            func on_save;
+        };
+
+        void add(callback* callback);
+        void remove(callback* callback);
+
+        void set(name value) {
+            path = value;
+        }
+        name get() const {
+            return str(path);
+        }
+
+        bool load();
+        bool save();
+
+    private:
+        string path;
+        callback::list callbacks;
+    };
+
+} // namespace lava
diff --git a/liblava/frame/frame.hpp b/liblava/frame/frame.hpp
index a84bcd7245f4413fa47a797c33d23b65c72ba706..588333860541f8995897c3749c7d0f62e09f7676 100644
--- a/liblava/frame/frame.hpp
+++ b/liblava/frame/frame.hpp
@@ -7,7 +7,7 @@
 #include <argh.h>
 #include <liblava/base/device.hpp>
 #include <liblava/base/instance.hpp>
-#include <liblava/file/file.hpp>
+#include <liblava/file.hpp>
 
 namespace lava {
 
diff --git a/liblava/frame/window.cpp b/liblava/frame/window.cpp
index 9be690894e65221730a83c74010d104d66075562..dd6630101a557a41f68f566a5dc3aebd96360b23 100644
--- a/liblava/frame/window.cpp
+++ b/liblava/frame/window.cpp
@@ -4,7 +4,7 @@
 
 #include <liblava/base/device.hpp>
 #include <liblava/base/instance.hpp>
-#include <liblava/file/file.hpp>
+#include <liblava/file.hpp>
 #include <liblava/frame/window.hpp>
 
 #define GLFW_INCLUDE_NONE
diff --git a/liblava/resource/mesh.cpp b/liblava/resource/mesh.cpp
index 2cda68277037434cd728a5f408c0eb93a86c249f..8f0a8020125b8e6c4f59fa9e97313a2e2c9a7014 100644
--- a/liblava/resource/mesh.cpp
+++ b/liblava/resource/mesh.cpp
@@ -2,7 +2,7 @@
 // copyright : Copyright (c) 2018-present, Lava Block OÜ
 // license   : MIT; see accompanying LICENSE file
 
-#include <liblava/file/file.hpp>
+#include <liblava/file.hpp>
 #include <liblava/resource/mesh.hpp>
 
 #ifndef LIBLAVA_TINYOBJLOADER
@@ -112,7 +112,7 @@ lava::mesh::ptr lava::load_mesh(device_ptr device, name filename) {
 
         string target_file = filename;
 
-        file_guard temp_file_remover;
+        file_remover temp_file_remover;
         {
             file file(filename);
             if (file.opened() && file.get_type() == file_type::fs) {
diff --git a/liblava/resource/texture.cpp b/liblava/resource/texture.cpp
index e3481a957e8338a559ed0feffd036b08eb3cb241..1cee8f079bc5817bf874af42ddc60f2d50f63389 100644
--- a/liblava/resource/texture.cpp
+++ b/liblava/resource/texture.cpp
@@ -3,6 +3,7 @@
 // license   : MIT; see accompanying LICENSE file
 
 #include <bitmap_image.hpp>
+#include <liblava/file/file_utils.hpp>
 #include <liblava/resource/format.hpp>
 #include <liblava/resource/texture.hpp>
 #include <selene/img/pixel/PixelTypeAliases.hpp>