diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 432ded89..e836ea2f 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -66,8 +66,8 @@ command.add(nil, { end, ["core:find-file"] = function() - if core.project_files_limit then - return command.perform "core:open-file" + if not core.project_files_number() then + return command.perform "core:open-file" end local files = {} for dir, item in core.get_project_files() do @@ -191,8 +191,6 @@ command.add(nil, { return end core.add_project_directory(system.absolute_path(text)) - -- TODO: add the name of directory to prioritize - core.reschedule_project_scan() end, suggest_directory) end, diff --git a/data/core/config.lua b/data/core/config.lua index 36d35c0a..3de48ee1 100644 --- a/data/core/config.lua +++ b/data/core/config.lua @@ -1,6 +1,5 @@ local config = {} -config.project_scan_rate = 5 config.fps = 60 config.max_log_items = 80 config.message_timeout = 5 diff --git a/data/core/init.lua b/data/core/init.lua index 7ab4629d..9ddf68e3 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -52,13 +52,6 @@ local function update_recents_project(action, dir_path_abs) end -function core.reschedule_project_scan() - if core.project_scan_thread_id then - core.threads[core.project_scan_thread_id].wake = 0 - end -end - - function core.set_project_dir(new_dir, change_project_fn) local chdir_ok = pcall(system.chdir, new_dir) if chdir_ok then @@ -66,9 +59,6 @@ function core.set_project_dir(new_dir, change_project_fn) core.project_dir = common.normalize_path(new_dir) core.project_directories = {} core.add_project_directory(new_dir) - core.project_files = {} - core.project_files_limit = false - core.reschedule_project_scan() return true end return false @@ -102,6 +92,20 @@ local function compare_file(a, b) return a.filename < b.filename end + +-- compute a file's info entry completed with "filename" to be used +-- in project scan or falsy if it shouldn't appear in the list. +local function get_project_file_info(root, file) + local info = system.get_file_info(root .. file) + if info then + info.filename = strip_leading_path(file) + return (info.size < config.file_size_limit * 1e6 and + not common.match_pattern(info.filename, config.ignore_files) + and info) + end +end + + -- "root" will by an absolute path without trailing '/' -- "path" will be a path starting with '/' and without trailing '/' -- or the empty string. @@ -110,34 +114,27 @@ end -- When recursing "root" will always be the same, only "path" will change. -- Returns a list of file "items". In eash item the "filename" will be the -- complete file path relative to "root" *without* the trailing '/'. -local function get_directory_files(root, path, t, recursive, begin_hook) +local function get_directory_files(dir, root, path, t, begin_hook, max_files) if begin_hook then begin_hook() end - local size_limit = config.file_size_limit * 10e5 local all = system.list_dir(root .. path) or {} local dirs, files = {}, {} local entries_count = 0 - local max_entries = config.max_project_files for _, file in ipairs(all) do - if not common.match_pattern(file, config.ignore_files) then - local file = path .. PATHSEP .. file - local info = system.get_file_info(root .. file) - if info and info.size < size_limit then - info.filename = strip_leading_path(file) - table.insert(info.type == "dir" and dirs or files, info) - entries_count = entries_count + 1 - if recursive and entries_count > max_entries then return nil, entries_count end - end + local info = get_project_file_info(root, path .. PATHSEP .. file) + if info then + table.insert(info.type == "dir" and dirs or files, info) + entries_count = entries_count + 1 end end table.sort(dirs, compare_file) for _, f in ipairs(dirs) do table.insert(t, f) - if recursive and entries_count <= max_entries then - local subdir_t, subdir_count = get_directory_files(root, PATHSEP .. f.filename, t, recursive) - entries_count = entries_count + subdir_count - f.scanned = true + if (not max_files or entries_count <= max_files) and core.project_subdir_is_shown(dir, f.filename) then + local sub_limit = max_files and max_files - entries_count + local _, n = get_directory_files(dir, root, PATHSEP .. f.filename, t, begin_hook, sub_limit) + entries_count = entries_count + n end end @@ -149,132 +146,289 @@ local function get_directory_files(root, path, t, recursive, begin_hook) return t, entries_count end -local function project_scan_thread() - local function diff_files(a, b) - if #a ~= #b then return true end - for i, v in ipairs(a) do - if b[i].filename ~= v.filename - or b[i].modified ~= v.modified then - return true - end - end - end - while true do - -- get project files and replace previous table if the new table is - -- different - local i = 1 - while not core.project_files_limit and i <= #core.project_directories do - local dir = core.project_directories[i] - local t, entries_count = get_directory_files(dir.name, "", {}, true) - if diff_files(dir.files, t) then - if entries_count > config.max_project_files then - core.project_files_limit = true - core.status_view:show_message("!", style.accent, - "Too many files in project directory: stopped reading at ".. - config.max_project_files.." files. For more information see ".. - "usage.md at github.com/franko/lite-xl." - ) - end - dir.files = t - core.redraw = true - end - if dir.name == core.project_dir then - core.project_files = dir.files - end - i = i + 1 +function core.project_subdir_set_show(dir, filename, show) + dir.shown_subdir[filename] = show + if dir.files_limit and PLATFORM == "Linux" then + local fullpath = dir.name .. PATHSEP .. filename + local watch_fn = show and system.watch_dir_add or system.watch_dir_rm + local success = watch_fn(dir.watch_id, fullpath) + if not success then + core.log("Internal warning: error calling system.watch_dir_%s", show and "add" or "rm") end - - -- wait for next scan - coroutine.yield(config.project_scan_rate) end end -function core.is_project_folder(dirname) - for _, dir in ipairs(core.project_directories) do - if dir.name == dirname then - return true - end - end - return false +function core.project_subdir_is_shown(dir, filename) + return not dir.files_limit or dir.shown_subdir[filename] end -function core.scan_project_folder(dirname, filename) - for _, dir in ipairs(core.project_directories) do - if dir.name == dirname then - for i, file in ipairs(dir.files) do - local file = dir.files[i] - if file.filename == filename then - if file.scanned then return end - local new_files = get_directory_files(dirname, PATHSEP .. filename, {}) - for j, new_file in ipairs(new_files) do - table.insert(dir.files, i + j, new_file) - end - file.scanned = true - return +local function show_max_files_warning() + core.status_view:show_message("!", style.accent, + "Too many files in project directory: stopped reading at ".. + config.max_project_files.." files. For more information see ".. + "usage.md at github.com/franko/lite-xl." + ) +end + +-- Populate a project folder top directory by scanning the filesystem. +local function scan_project_folder(index) + local dir = core.project_directories[index] + local t, entries_count = get_directory_files(dir, dir.name, "", {}, nil, config.max_project_files) + if entries_count > config.max_project_files then + dir.files_limit = true + -- Watch non-recursively on Linux only. + -- The reason is recursively watching with dmon on linux + -- doesn't work on very large directories. + dir.watch_id = system.watch_dir(dir.name, PLATFORM ~= "Linux") + if core.status_view then -- May be not yet initialized. + show_max_files_warning() + end + else + dir.watch_id = system.watch_dir(dir.name, true) + end + dir.files = t + core.dir_rescan_add_job(dir, ".") +end + + +function core.add_project_directory(path) + -- top directories has a file-like "item" but the item.filename + -- will be simply the name of the directory, without its path. + -- The field item.topdir will identify it as a top level directory. + path = common.normalize_path(path) + local dir = { + name = path, + item = {filename = common.basename(path), type = "dir", topdir = true}, + files_limit = false, + is_dirty = true, + shown_subdir = {}, + } + table.insert(core.project_directories, dir) + scan_project_folder(#core.project_directories) + if path == core.project_dir then + core.project_files = dir.files + end + core.redraw = true +end + + +local function file_search(files, info) + local filename, type = info.filename, info.type + local inf, sup = 1, #files + while sup - inf > 8 do + local curr = math.floor((inf + sup) / 2) + if system.path_compare(filename, type, files[curr].filename, files[curr].type) then + sup = curr - 1 + else + inf = curr + end + end + repeat + if files[inf].filename == filename then + return inf, true + end + inf = inf + 1 + until inf > sup or system.path_compare(filename, type, files[inf].filename, files[inf].type) + return inf, false +end + + +local function project_scan_add_entry(dir, fileinfo) + local index, match = file_search(dir.files, fileinfo) + if not match then + table.insert(dir.files, index, fileinfo) + dir.is_dirty = true + end +end + + +local function files_info_equal(a, b) + return a.filename == b.filename and a.type == b.type +end + +-- for "a" inclusive from i1 + 1 and i1 + n +local function files_list_match(a, i1, n, b) + if n ~= #b then return false end + for i = 1, n do + if not files_info_equal(a[i1 + i], b[i]) then + return false + end + end + return true +end + +-- arguments like for files_list_match +local function files_list_replace(as, i1, n, bs) + local m = #bs + local i, j = 1, 1 + while i <= m or i <= n do + local a, b = as[i1 + i], bs[j] + if i > n or (j <= m and not files_info_equal(a, b) and + not system.path_compare(a.filename, a.type, b.filename, b.type)) + then + table.insert(as, i1 + i, b) + i, j, n = i + 1, j + 1, n + 1 + elseif j > m or system.path_compare(a.filename, a.type, b.filename, b.type) then + table.remove(as, i1 + i) + n = n - 1 + else + i, j = i + 1, j + 1 + end + end +end + +local function project_subdir_bounds(dir, filename) + local index, n = 0, #dir.files + for i, file in ipairs(dir.files) do + local file = dir.files[i] + if file.filename == filename then + index, n = i, #dir.files - i + for j = 1, #dir.files - i do + if not common.path_belongs_to(dir.files[i + j].filename, filename) then + n = j - 1 + break end end + return index, n, file end end end +local function rescan_project_subdir(dir, filename_rooted) + local new_files = get_directory_files(dir, dir.name, filename_rooted, {}, coroutine.yield) + local index, n = 0, #dir.files + if filename_rooted ~= "" then + local filename = strip_leading_path(filename_rooted) + index, n = project_subdir_bounds(dir, filename) + end -local function find_project_files_co(root, path) - local size_limit = config.file_size_limit * 10e5 + if not files_list_match(dir.files, index, n, new_files) then + files_list_replace(dir.files, index, n, new_files) + dir.is_dirty = true + return true + end +end + + +function core.update_project_subdir(dir, filename, expanded) + local index, n, file = project_subdir_bounds(dir, filename) + if index then + local new_files = expanded and get_directory_files(dir, dir.name, PATHSEP .. filename, {}) or {} + files_list_replace(dir.files, index, n, new_files) + dir.is_dirty = true + return true + end +end + + +-- Find files and directories recursively reading from the filesystem. +-- Filter files and yields file's directory and info table. This latter +-- is filled to be like required by project directories "files" list. +local function find_files_rec(root, path) local all = system.list_dir(root .. path) or {} for _, file in ipairs(all) do - if not common.match_pattern(file, config.ignore_files) then - local file = path .. PATHSEP .. file - local info = system.get_file_info(root .. file) - if info and info.size < size_limit then - info.filename = strip_leading_path(file) - if info.type == "file" then - coroutine.yield(root, info) - else - find_project_files_co(root, PATHSEP .. info.filename) - end + local file = path .. PATHSEP .. file + local info = system.get_file_info(root .. file) + if info then + info.filename = strip_leading_path(file) + if info.type == "file" then + coroutine.yield(root, info) + else + find_files_rec(root, PATHSEP .. info.filename) end end end end +-- Iterator function to list all project files local function project_files_iter(state) local dir = core.project_directories[state.dir_index] - state.file_index = state.file_index + 1 - while dir and state.file_index > #dir.files do - state.dir_index = state.dir_index + 1 - state.file_index = 1 - dir = core.project_directories[state.dir_index] + if state.co then + -- We have a coroutine to fetch for files, use the coroutine. + -- Used for directories that exceeds the files nuumber limit. + local ok, name, file = coroutine.resume(state.co, dir.name, "") + if ok and name then + return name, file + else + -- The coroutine terminated, increment file/dir counter to scan + -- next project directory. + state.co = false + state.file_index = 1 + state.dir_index = state.dir_index + 1 + dir = core.project_directories[state.dir_index] + end + else + -- Increase file/dir counter + state.file_index = state.file_index + 1 + while dir and state.file_index > #dir.files do + state.dir_index = state.dir_index + 1 + state.file_index = 1 + dir = core.project_directories[state.dir_index] + end end if not dir then return end + if dir.files_limit then + -- The current project directory is files limited: create a couroutine + -- to read files from the filesystem. + state.co = coroutine.create(find_files_rec) + return project_files_iter(state) + end return dir.name, dir.files[state.file_index] end function core.get_project_files() - if core.project_files_limit then - return coroutine.wrap(function() - for _, dir in ipairs(core.project_directories) do - find_project_files_co(dir.name, "") - end - end) - else - local state = { dir_index = 1, file_index = 0 } - return project_files_iter, state - end + local state = { dir_index = 1, file_index = 0 } + return project_files_iter, state end function core.project_files_number() - if not core.project_files_limit then - local n = 0 - for i = 1, #core.project_directories do - n = n + #core.project_directories[i].files + local n = 0 + for i = 1, #core.project_directories do + if core.project_directories[i].files_limit then return end + n = n + #core.project_directories[i].files + end + return n +end + + +local function project_dir_by_watch_id(watch_id) + for i = 1, #core.project_directories do + if core.project_directories[i].watch_id == watch_id then + return core.project_directories[i] end - return n + end +end + + +local function project_scan_remove_file(dir, filepath) + local fileinfo = { filename = filepath } + for _, filetype in ipairs {"dir", "file"} do + fileinfo.type = filetype + local index, match = file_search(dir.files, fileinfo) + if match then + table.remove(dir.files, index) + dir.is_dirty = true + return + end + end +end + + +local function project_scan_add_file(dir, filepath) + for fragment in string.gmatch(filepath, "([^/\\]+)") do + if common.match_pattern(fragment, config.ignore_files) then + return + end + end + local fileinfo = get_project_file_info(dir.name, PATHSEP .. filepath) + if fileinfo then + project_scan_add_entry(dir, fileinfo) end end @@ -371,19 +525,6 @@ function core.load_user_directory() end -function core.add_project_directory(path) - -- top directories has a file-like "item" but the item.filename - -- will be simply the name of the directory, without its path. - -- The field item.topdir will identify it as a top level directory. - path = common.normalize_path(path) - table.insert(core.project_directories, { - name = path, - item = {filename = common.basename(path), type = "dir", topdir = true}, - files = {} - }) -end - - function core.remove_project_directory(path) -- skip the fist directory because it is the project's directory for i = 2, #core.project_directories do @@ -519,7 +660,6 @@ function core.init() cur_node = cur_node:split("down", core.command_view, {y = true}) cur_node = cur_node:split("down", core.status_view, {y = true}) - core.project_scan_thread_id = core.add_thread(project_scan_thread) command.add_defaults() local got_user_error = not core.load_user_directory() local plugins_success, plugins_refuse_list = core.load_plugins() @@ -530,6 +670,12 @@ function core.init() end local got_project_error = not core.load_project_module() + -- We assume we have just a single project directory here. Now that StatusView + -- is there show max files warning if needed. + if core.project_directories[1].files_limit then + show_max_files_warning() + end + for _, filename in ipairs(files) do core.root_view:open_doc(core.open_doc(filename)) end @@ -918,6 +1064,76 @@ function core.try(fn, ...) return false, err end +local scheduled_rescan = {} + +function core.has_pending_rescan() + for _ in pairs(scheduled_rescan) do + return true + end +end + + +function core.dir_rescan_add_job(dir, filepath) + local dirpath = filepath:match("^(.+)[/\\].+$") + local dirpath_rooted = dirpath and PATHSEP .. dirpath or "" + local abs_dirpath = dir.name .. dirpath_rooted + if dirpath then + -- check if the directory is in the project files list, if not exit + local dir_index, dir_match = file_search(dir.files, {filename = dirpath, type = "dir"}) + -- Note that is dir_match is false dir_index greaten than the last valid index. + -- We use dir_index to index dir.files below only if dir_match is true. + if not dir_match or not core.project_subdir_is_shown(dir, dir.files[dir_index].filename) then return end + end + local new_time = system.get_time() + 1 + + -- evaluate new rescan request versus existing rescan + local remove_list = {} + for _, rescan in pairs(scheduled_rescan) do + if abs_dirpath == rescan.abs_path or common.path_belongs_to(abs_dirpath, rescan.abs_path) then + -- abs_dirpath is a subpath of a scan already ongoing: skip + rescan.time_limit = new_time + return + elseif common.path_belongs_to(rescan.abs_path, abs_dirpath) then + -- abs_dirpath already cover this rescan: add to the list of rescan to be removed + table.insert(remove_list, rescan.abs_path) + end + end + for _, key_path in ipairs(remove_list) do + scheduled_rescan[key_path] = nil + end + + scheduled_rescan[abs_dirpath] = {dir = dir, path = dirpath_rooted, abs_path = abs_dirpath, time_limit = new_time} + core.add_thread(function() + while true do + local rescan = scheduled_rescan[abs_dirpath] + if not rescan then return end + if system.get_time() > rescan.time_limit then + local has_changes = rescan_project_subdir(rescan.dir, rescan.path) + if has_changes then + core.redraw = true -- we run without an event, from a thread + rescan.time_limit = new_time + else + scheduled_rescan[rescan.abs_path] = nil + return + end + end + coroutine.yield(0.2) + end + end) +end + + +function core.on_dir_change(watch_id, action, filepath) + local dir = project_dir_by_watch_id(watch_id) + if not dir then return end + core.dir_rescan_add_job(dir, filepath) + if action == "delete" then + project_scan_remove_file(dir, filepath) + elseif action == "create" then + project_scan_add_file(dir, filepath) + end +end + function core.on_event(type, ...) local did_keymap = false @@ -954,6 +1170,8 @@ function core.on_event(type, ...) end elseif type == "focuslost" then core.root_view:on_focus_lost(...) + elseif type == "dirchange" then + core.on_dir_change(...) elseif type == "quit" then core.quit() end @@ -1060,7 +1278,7 @@ function core.run() while true do core.frame_start = system.get_time() local did_redraw = core.step() - local need_more_work = run_threads() + local need_more_work = run_threads() or core.has_pending_rescan() if core.restart_request or core.quit_request then break end if not did_redraw and not need_more_work then idle_iterations = idle_iterations + 1 diff --git a/data/plugins/autoreload.lua b/data/plugins/autoreload.lua index e772666f..55a2d99e 100644 --- a/data/plugins/autoreload.lua +++ b/data/plugins/autoreload.lua @@ -3,9 +3,10 @@ local core = require "core" local config = require "core.config" local Doc = require "core.doc" - local times = setmetatable({}, { __mode = "k" }) +local autoreload_scan_rate = 5 + local function update_time(doc) local info = system.get_file_info(doc.filename) times[doc] = info.modified @@ -40,7 +41,7 @@ core.add_thread(function() end -- wait for next scan - coroutine.yield(config.project_scan_rate) + coroutine.yield(autoreload_scan_rate) end end) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index fa3ab53a..d08db03e 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -41,7 +41,6 @@ function TreeView:new() self.init_size = true self.target_size = default_treeview_size self.cache = {} - self.last = {} self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 } end @@ -54,7 +53,7 @@ function TreeView:set_target_size(axis, value) end -function TreeView:get_cached(item, dirname) +function TreeView:get_cached(dir, item, dirname) local dir_cache = self.cache[dirname] if not dir_cache then dir_cache = {} @@ -80,6 +79,7 @@ function TreeView:get_cached(item, dirname) end t.name = basename t.type = item.type + t.dir = dir -- points to top level "dir" item dir_cache[cache_name] = t end return t @@ -104,18 +104,13 @@ end function TreeView:check_cache() - -- invalidate cache's skip values if project_files has changed for i = 1, #core.project_directories do local dir = core.project_directories[i] - local last_files = self.last[dir.name] - if not last_files then - self.last[dir.name] = dir.files - else - if dir.files ~= last_files then - self:invalidate_cache(dir.name) - self.last[dir.name] = dir.files - end + -- invalidate cache's skip values if directory is declared dirty + if dir.is_dirty and self.cache[dir.name] then + self:invalidate_cache(dir.name) end + dir.is_dirty = false end end @@ -131,14 +126,14 @@ function TreeView:each_item() for k = 1, #core.project_directories do local dir = core.project_directories[k] - local dir_cached = self:get_cached(dir.item, dir.name) + local dir_cached = self:get_cached(dir, dir.item, dir.name) coroutine.yield(dir_cached, ox, y, w, h) count_lines = count_lines + 1 y = y + h local i = 1 while i <= #dir.files and dir_cached.expanded do local item = dir.files[i] - local cached = self:get_cached(item, dir.name) + local cached = self:get_cached(dir, item, dir.name) coroutine.yield(cached, ox, y, w, h) count_lines = count_lines + 1 @@ -206,7 +201,6 @@ local function create_directory_in(item) core.error("cannot create directory %q: %s", dirname, err) end item.expanded = true - core.reschedule_project_scan() end) end @@ -223,23 +217,11 @@ function TreeView:on_mouse_pressed(button, x, y, clicks) if keymap.modkeys["ctrl"] and button == "left" then create_directory_in(hovered_item) else - if core.project_files_limit and not hovered_item.expanded then - local filename, abs_filename = hovered_item.filename, hovered_item.abs_filename - local index = 0 - -- The loop below is used to find the first match starting from the end - -- in case there are multiple matches. - while index and index + #filename < #abs_filename do - index = string.find(abs_filename, filename, index + 1, true) - end - -- we assume here index is not nil because the abs_filename must contain the - -- relative filename - local dirname = string.sub(abs_filename, 1, index - 2) - if core.is_project_folder(dirname) then - core.scan_project_folder(dirname, filename) - self:invalidate_cache(dirname) - end - end hovered_item.expanded = not hovered_item.expanded + if hovered_item.dir.files_limit then + core.update_project_subdir(hovered_item.dir, hovered_item.filename, hovered_item.expanded) + core.project_subdir_set_show(hovered_item.dir, hovered_item.filename, hovered_item.expanded) + end end else core.try(function() @@ -461,7 +443,6 @@ command.add(function() return view.hovered_item ~= nil end, { else core.error("Error while renaming \"%s\" to \"%s\": %s", old_abs_filename, abs_filename, err) end - core.reschedule_project_scan() end, common.path_suggest) end, @@ -476,7 +457,6 @@ command.add(function() return view.hovered_item ~= nil end, { file:write("") file:close() core.root_view:open_doc(core.open_doc(doc_filename)) - core.reschedule_project_scan() core.log("Created %s", doc_filename) end, common.path_suggest) end, @@ -489,7 +469,6 @@ command.add(function() return view.hovered_item ~= nil end, { core.command_view:enter("Folder Name", function(filename) local dir_path = core.project_dir .. PATHSEP .. filename common.mkdirp(dir_path) - core.reschedule_project_scan() core.log("Created %s", dir_path) end, common.path_suggest) end, @@ -526,7 +505,6 @@ command.add(function() return view.hovered_item ~= nil end, { return end end - core.reschedule_project_scan() core.log("Deleted \"%s\"", filename) end end diff --git a/licenses/licenses.md b/licenses/licenses.md index 8005c4a7..928d88d9 100644 --- a/licenses/licenses.md +++ b/licenses/licenses.md @@ -22,6 +22,33 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +## septag/dmon + +Copyright 2019 Sepehr Taghdisian. All rights reserved. + +https://github.com/septag/dmon + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ## Fira Sans Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. diff --git a/meson.build b/meson.build index 41736101..f1d2b354 100644 --- a/meson.build +++ b/meson.build @@ -45,6 +45,7 @@ endif if not get_option('source-only') libm = cc.find_library('m', required : false) libdl = cc.find_library('dl', required : false) + threads_dep = dependency('threads') lua_dep = dependency('lua5.2', fallback: ['lua', 'lua_dep'], default_options: ['shared=false', 'use_readline=false', 'app=false'] ) @@ -57,7 +58,7 @@ if not get_option('source-only') ] ) - lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl] + lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl, threads_dep] if host_machine.system() == 'windows' # Note that we need to explicitly add the windows socket DLL because diff --git a/resources/notes-dmon-integration.md b/resources/notes-dmon-integration.md new file mode 100644 index 00000000..5179df40 --- /dev/null +++ b/resources/notes-dmon-integration.md @@ -0,0 +1,54 @@ + +`core.set_project_dir`: + Reset project directories and set its directory. + It chdir into the directory, empty the `core.project_directories` and add + the given directory. + `core.add_project_directory`: + Add a new top-level directory to the project. + Also called from modules and commands outside core.init. + local function `scan_project_folder`: + Scan all files for a given top-level project directory. + Can emit a warning about file limit. + Called only from within core.init module. + +`core.scan_project_subdir`: (before was named `core.scan_project_folder`) + scan a single folder, without recursion. Used when too many files. + +Local function `scan_project_folder`: + Populate the project folder top directory. Done only once when the directory + is added to the project. + +`core.add_project_directory`: + Add a new top-level folder to the project. + +`core.set_project_dir`: + Set the initial project directory. + +`core.dir_rescan_add_job`: + Add a job to rescan after an elapsed time a project's subdirectory to fix for any + changes. + +Local function `rescan_project_subdir`: + Rescan a project's subdirectory, compare to the current version and patch the list if + a difference is found. + + +`core.project_scan_thread`: + Should disappear now that we use dmon. + + +`core.project_scan_topdir`: + New function to scan a top level project folder. + + +`config.project_scan_rate`: +`core.project_scan_thread_id`: +`core.reschedule_project_scan`: +`core.project_files_limit`: + A eliminer. + +`core.get_project_files`: + To be fixed. Use `find_project_files_co` for a single directory + +In TreeView remove usage of self.last to detect new scan that changed the files list. + diff --git a/src/api/system.c b/src/api/system.c index d84f86dd..5b72b4d8 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -6,6 +6,7 @@ #include #include #include "api.h" +#include "dirmonitor.h" #include "rencache.h" #ifdef _WIN32 #include @@ -236,6 +237,14 @@ top: lua_pushnumber(L, e.wheel.y); return 2; + case SDL_USEREVENT: + lua_pushstring(L, "dirchange"); + lua_pushnumber(L, e.user.code >> 16); + lua_pushstring(L, (e.user.code & 0xffff) == DMON_ACTION_DELETE ? "delete" : "create"); + lua_pushstring(L, e.user.data1); + free(e.user.data1); + return 4; + default: goto top; } @@ -651,6 +660,91 @@ static int f_set_window_opacity(lua_State *L) { return 1; } +static int f_watch_dir(lua_State *L) { + const char *path = luaL_checkstring(L, 1); + const int recursive = lua_toboolean(L, 2); + uint32_t dmon_flags = (recursive ? DMON_WATCHFLAGS_RECURSIVE : 0); + dmon_watch_id watch_id = dmon_watch(path, dirmonitor_watch_callback, dmon_flags, NULL); + if (watch_id.id == 0) { luaL_error(L, "directory monitoring watch failed"); } + lua_pushnumber(L, watch_id.id); + return 1; +} + +#if __linux__ +static int f_watch_dir_add(lua_State *L) { + dmon_watch_id watch_id; + watch_id.id = luaL_checkinteger(L, 1); + const char *subdir = luaL_checkstring(L, 2); + lua_pushboolean(L, dmon_watch_add(watch_id, subdir)); + return 1; +} + +static int f_watch_dir_rm(lua_State *L) { + dmon_watch_id watch_id; + watch_id.id = luaL_checkinteger(L, 1); + const char *subdir = luaL_checkstring(L, 2); + lua_pushboolean(L, dmon_watch_rm(watch_id, subdir)); + return 1; +} +#endif + +#ifdef _WIN32 +#define PATHSEP '\\' +#else +#define PATHSEP '/' +#endif + +/* Special purpose filepath compare function. Corresponds to the + order used in the TreeView view of the project's files. Returns true iff + path1 < path2 in the TreeView order. */ +static int f_path_compare(lua_State *L) { + const char *path1 = luaL_checkstring(L, 1); + const char *type1_s = luaL_checkstring(L, 2); + const char *path2 = luaL_checkstring(L, 3); + const char *type2_s = luaL_checkstring(L, 4); + const int len1 = strlen(path1), len2 = strlen(path2); + int type1 = strcmp(type1_s, "dir") != 0; + int type2 = strcmp(type2_s, "dir") != 0; + /* Find the index of the common part of the path. */ + int offset = 0, i; + for (i = 0; i < len1 && i < len2; i++) { + if (path1[i] != path2[i]) break; + if (path1[i] == PATHSEP) { + offset = i + 1; + } + } + /* If a path separator is present in the name after the common part we consider + the entry like a directory. */ + if (strchr(path1 + offset, PATHSEP)) { + type1 = 0; + } + if (strchr(path2 + offset, PATHSEP)) { + type2 = 0; + } + /* If types are different "dir" types comes before "file" types. */ + if (type1 != type2) { + lua_pushboolean(L, type1 < type2); + return 1; + } + /* If types are the same compare the files' path alphabetically. */ + int cfr = 0; + int len_min = (len1 < len2 ? len1 : len2); + for (int j = offset; j <= len_min; j++) { + if (path1[j] == path2[j]) continue; + if (path1[j] == 0 || path2[j] == 0) { + cfr = (path1[j] == 0); + } else if (path1[j] == PATHSEP || path2[j] == PATHSEP) { + /* For comparison we treat PATHSEP as if it was the string terminator. */ + cfr = (path1[j] == PATHSEP); + } else { + cfr = (path1[j] < path2[j]); + } + break; + } + lua_pushboolean(L, cfr); + return 1; +} + static const luaL_Reg lib[] = { { "poll_event", f_poll_event }, @@ -678,6 +772,12 @@ static const luaL_Reg lib[] = { { "exec", f_exec }, { "fuzzy_match", f_fuzzy_match }, { "set_window_opacity", f_set_window_opacity }, + { "watch_dir", f_watch_dir }, + { "path_compare", f_path_compare }, +#if __linux__ + { "watch_dir_add", f_watch_dir_add }, + { "watch_dir_rm", f_watch_dir_rm }, +#endif { NULL, NULL } }; diff --git a/src/dirmonitor.c b/src/dirmonitor.c new file mode 100644 index 00000000..eb3b185f --- /dev/null +++ b/src/dirmonitor.c @@ -0,0 +1,60 @@ +#include +#include + +#include + +#define DMON_IMPL +#include "dmon.h" + +#include "dirmonitor.h" + +static void send_sdl_event(dmon_watch_id watch_id, dmon_action action, const char *filepath) { + SDL_Event ev; + const int size = strlen(filepath) + 1; + /* The string allocated below should be deallocated as soon as the event is + treated in the SDL main loop. */ + char *new_filepath = malloc(size); + if (!new_filepath) return; + memcpy(new_filepath, filepath, size); +#ifdef _WIN32 + for (int i = 0; i < size; i++) { + if (new_filepath[i] == '/') { + new_filepath[i] = '\\'; + } + } +#endif + SDL_zero(ev); + ev.type = SDL_USEREVENT; + ev.user.code = ((watch_id.id & 0xffff) << 16) | (action & 0xffff); + ev.user.data1 = new_filepath; + SDL_PushEvent(&ev); +} + +void dirmonitor_init() { + dmon_init(); + /* In theory we should register our user event but since we + have just one type of user event this is not really needed. */ + /* sdl_dmon_event_type = SDL_RegisterEvents(1); */ +} + +void dirmonitor_deinit() { + dmon_deinit(); +} + +void dirmonitor_watch_callback(dmon_watch_id watch_id, dmon_action action, const char *rootdir, + const char *filepath, const char *oldfilepath, void *user) +{ + (void) rootdir; + (void) user; + switch (action) { + case DMON_ACTION_MOVE: + send_sdl_event(watch_id, DMON_ACTION_DELETE, oldfilepath); + send_sdl_event(watch_id, DMON_ACTION_CREATE, filepath); + break; + case DMON_ACTION_MODIFY: + break; + default: + send_sdl_event(watch_id, action, filepath); + } +} + diff --git a/src/dirmonitor.h b/src/dirmonitor.h new file mode 100644 index 00000000..ab9376c0 --- /dev/null +++ b/src/dirmonitor.h @@ -0,0 +1,14 @@ +#ifndef DIRMONITOR_H +#define DIRMONITOR_H + +#include + +#include "dmon.h" + +void dirmonitor_init(); +void dirmonitor_deinit(); +void dirmonitor_watch_callback(dmon_watch_id watch_id, dmon_action action, const char *rootdir, + const char *filepath, const char *oldfilepath, void *user); + +#endif + diff --git a/src/dmon.h b/src/dmon.h new file mode 100644 index 00000000..1ccae446 --- /dev/null +++ b/src/dmon.h @@ -0,0 +1,1706 @@ +// +// Copyright 2021 Sepehr Taghdisian (septag@github). All rights reserved. +// License: https://github.com/septag/dmon#license-bsd-2-clause +// +// Portable directory monitoring library +// watches directories for file or directory changes. +// +// Usage: +// define DMON_IMPL and include this file to use it: +// #define DMON_IMPL +// #include "dmon.h" +// +// dmon_init(): +// Call this once at the start of your program. +// This will start a low-priority monitoring thread +// dmon_deinit(): +// Call this when your work with dmon is finished, usually on program terminate +// This will free resources and stop the monitoring thread +// dmon_watch: +// Watch for directories +// You can watch multiple directories by calling this function multiple times +// rootdir: root directory to monitor +// watch_cb: callback function to receive events. +// NOTE that this function is called from another thread, so you should +// beware of data races in your application when accessing data within this +// callback +// flags: watch flags, see dmon_watch_flags_t +// user_data: user pointer that is passed to callback function +// Returns the Id of the watched directory after successful call, or returns Id=0 if error +// dmon_unwatch: +// Remove the directory from watch list +// +// see test.c for the basic example +// +// Configuration: +// You can customize some low-level functionality like malloc and logging by overriding macros: +// +// DMON_MALLOC, DMON_FREE, DMON_REALLOC: +// define these macros to override memory allocations +// default is 'malloc', 'free' and 'realloc' +// DMON_ASSERT: +// define this to provide your own assert +// default is 'assert' +// DMON_LOG_ERROR: +// define this to provide your own logging mechanism +// default implementation logs to stdout and breaks the program +// DMON_LOG_DEBUG +// define this to provide your own extra debug logging mechanism +// default implementation logs to stdout in DEBUG and does nothing in other builds +// DMON_API_DECL, DMON_API_IMPL +// define these to provide your own API declerations. (for example: static) +// default is nothing (which is extern in C language ) +// DMON_MAX_PATH +// Maximum size of path characters +// default is 260 characters +// DMON_MAX_WATCHES +// Maximum number of watch directories +// default is 64 +// +// TODO: +// - DMON_WATCHFLAGS_FOLLOW_SYMLINKS does not resolve files +// - implement DMON_WATCHFLAGS_OUTOFSCOPE_LINKS +// - implement DMON_WATCHFLAGS_IGNORE_DIRECTORIES +// +// History: +// 1.0.0 First version. working Win32/Linux backends +// 1.1.0 MacOS backend +// 1.1.1 Minor fixes, eliminate gcc/clang warnings with -Wall +// 1.1.2 Eliminate some win32 dead code +// 1.1.3 Fixed select not resetting causing high cpu usage on linux +// +#ifndef __DMON_H__ +#define __DMON_H__ + +#include +#include + +#ifndef DMON_API_DECL +# define DMON_API_DECL +#endif + +#ifndef DMON_API_IMPL +# define DMON_API_IMPL +#endif + +typedef struct { uint32_t id; } dmon_watch_id; + +// Pass these flags to `dmon_watch` +typedef enum dmon_watch_flags_t { + DMON_WATCHFLAGS_RECURSIVE = 0x1, // monitor all child directories + DMON_WATCHFLAGS_FOLLOW_SYMLINKS = 0x2, // resolve symlinks (linux only) + DMON_WATCHFLAGS_OUTOFSCOPE_LINKS = 0x4, // TODO: not implemented yet + DMON_WATCHFLAGS_IGNORE_DIRECTORIES = 0x8 // TODO: not implemented yet +} dmon_watch_flags; + +// Action is what operation performed on the file. this value is provided by watch callback +typedef enum dmon_action_t { + DMON_ACTION_CREATE = 1, + DMON_ACTION_DELETE, + DMON_ACTION_MODIFY, + DMON_ACTION_MOVE +} dmon_action; + +#ifdef __cplusplus +extern "C" { +#endif + +DMON_API_DECL void dmon_init(void); +DMON_API_DECL void dmon_deinit(void); + +DMON_API_DECL dmon_watch_id dmon_watch(const char* rootdir, + void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, + const char* rootdir, const char* filepath, + const char* oldfilepath, void* user), + uint32_t flags, void* user_data); +DMON_API_DECL void dmon_unwatch(dmon_watch_id id); +DMON_API_DECL bool dmon_watch_add(dmon_watch_id id, const char* subdir); +DMON_API_DECL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir); + +#ifdef __cplusplus +} +#endif + +#ifdef DMON_IMPL + +#define DMON_OS_WINDOWS 0 +#define DMON_OS_MACOS 0 +#define DMON_OS_LINUX 0 + +#if defined(_WIN32) || defined(_WIN64) +# undef DMON_OS_WINDOWS +# define DMON_OS_WINDOWS 1 +#elif defined(__linux__) +# undef DMON_OS_LINUX +# define DMON_OS_LINUX 1 +#elif defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) +# undef DMON_OS_MACOS +# define DMON_OS_MACOS __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ +#else +# define DMON_OS 0 +# error "unsupported platform" +#endif + +#if DMON_OS_WINDOWS +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# ifndef NOMINMAX +# define NOMINMAX +# endif +# include +# include +# ifdef _MSC_VER +# pragma intrinsic(_InterlockedExchange) +# endif +#elif DMON_OS_LINUX +# ifndef __USE_MISC +# define __USE_MISC +# endif +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +#elif DMON_OS_MACOS +# include +# include +# include +# include +# include +#endif + +#ifndef DMON_MALLOC +# include +# define DMON_MALLOC(size) malloc(size) +# define DMON_FREE(ptr) free(ptr) +# define DMON_REALLOC(ptr, size) realloc(ptr, size) +#endif + +#ifndef DMON_ASSERT +# include +# define DMON_ASSERT(e) assert(e) +#endif + +#ifndef DMON_LOG_ERROR +# include +# define DMON_LOG_ERROR(s) do { puts(s); DMON_ASSERT(0); } while(0) +#endif + +#ifndef DMON_LOG_DEBUG +# ifndef NDEBUG +# include +# define DMON_LOG_DEBUG(s) do { puts(s); } while(0) +# else +# define DMON_LOG_DEBUG(s) +# endif +#endif + +#ifndef DMON_MAX_WATCHES +# define DMON_MAX_WATCHES 64 +#endif + +#ifndef DMON_MAX_PATH +# define DMON_MAX_PATH 260 +#endif + +#define _DMON_UNUSED(x) (void)(x) + +#ifndef _DMON_PRIVATE +# if defined(__GNUC__) || defined(__clang__) +# define _DMON_PRIVATE __attribute__((unused)) static +# else +# define _DMON_PRIVATE static +# endif +#endif + +#include + +#ifndef _DMON_LOG_ERRORF +# define _DMON_LOG_ERRORF(str, ...) do { char msg[512]; snprintf(msg, sizeof(msg), str, __VA_ARGS__); DMON_LOG_ERROR(msg); } while(0); +#endif + +#ifndef _DMON_LOG_DEBUGF +# define _DMON_LOG_DEBUGF(str, ...) do { char msg[512]; snprintf(msg, sizeof(msg), str, __VA_ARGS__); DMON_LOG_DEBUG(msg); } while(0); +#endif + +#ifndef dmon__min +# define dmon__min(a, b) ((a) < (b) ? (a) : (b)) +#endif + +#ifndef dmon__max +# define dmon__max(a, b) ((a) > (b) ? (a) : (b)) +#endif + +#ifndef dmon__swap +# define dmon__swap(a, b, _type) \ + do { \ + _type tmp = a; \ + a = b; \ + b = tmp; \ + } while (0) +#endif + +#ifndef dmon__make_id +# ifdef __cplusplus +# define dmon__make_id(id) {id} +# else +# define dmon__make_id(id) (dmon_watch_id) {id} +# endif +#endif // dmon__make_id + +_DMON_PRIVATE bool dmon__isrange(char ch, char from, char to) +{ + return (uint8_t)(ch - from) <= (uint8_t)(to - from); +} + +_DMON_PRIVATE bool dmon__isupperchar(char ch) +{ + return dmon__isrange(ch, 'A', 'Z'); +} + +_DMON_PRIVATE char dmon__tolowerchar(char ch) +{ + return ch + (dmon__isupperchar(ch) ? 0x20 : 0); +} + +_DMON_PRIVATE char* dmon__tolower(char* dst, int dst_sz, const char* str) +{ + int offset = 0; + int dst_max = dst_sz - 1; + while (*str && offset < dst_max) { + dst[offset++] = dmon__tolowerchar(*str); + ++str; + } + dst[offset] = '\0'; + return dst; +} + +_DMON_PRIVATE char* dmon__strcpy(char* dst, int dst_sz, const char* src) +{ + DMON_ASSERT(dst); + DMON_ASSERT(src); + + const int32_t len = (int32_t)strlen(src); + const int32_t _max = dst_sz - 1; + const int32_t num = (len < _max ? len : _max); + memcpy(dst, src, num); + dst[num] = '\0'; + + return dst; +} + +_DMON_PRIVATE char* dmon__unixpath(char* dst, int size, const char* path) +{ + size_t len = strlen(path); + len = dmon__min(len, (size_t)size - 1); + + for (size_t i = 0; i < len; i++) { + if (path[i] != '\\') + dst[i] = path[i]; + else + dst[i] = '/'; + } + dst[len] = '\0'; + return dst; +} + +#if DMON_OS_LINUX || DMON_OS_MACOS +_DMON_PRIVATE char* dmon__strcat(char* dst, int dst_sz, const char* src) +{ + int len = (int)strlen(dst); + return dmon__strcpy(dst + len, dst_sz - len, src); +} +#endif // DMON_OS_LINUX || DMON_OS_MACOS + +// stretchy buffer: https://github.com/nothings/stb/blob/master/stretchy_buffer.h +#define stb_sb_free(a) ((a) ? DMON_FREE(stb__sbraw(a)),0 : 0) +#define stb_sb_push(a,v) (stb__sbmaybegrow(a,1), (a)[stb__sbn(a)++] = (v)) +#define stb_sb_count(a) ((a) ? stb__sbn(a) : 0) +#define stb_sb_add(a,n) (stb__sbmaybegrow(a,n), stb__sbn(a)+=(n), &(a)[stb__sbn(a)-(n)]) +#define stb_sb_last(a) ((a)[stb__sbn(a)-1]) +#define stb_sb_reset(a) ((a) ? (stb__sbn(a) = 0) : 0) + +#define stb__sbraw(a) ((int *) (a) - 2) +#define stb__sbm(a) stb__sbraw(a)[0] +#define stb__sbn(a) stb__sbraw(a)[1] + +#define stb__sbneedgrow(a,n) ((a)==0 || stb__sbn(a)+(n) >= stb__sbm(a)) +#define stb__sbmaybegrow(a,n) (stb__sbneedgrow(a,(n)) ? stb__sbgrow(a,n) : 0) +#define stb__sbgrow(a,n) (*((void **)&(a)) = stb__sbgrowf((a), (n), sizeof(*(a)))) + +static void * stb__sbgrowf(void *arr, int increment, int itemsize) +{ + int dbl_cur = arr ? 2*stb__sbm(arr) : 0; + int min_needed = stb_sb_count(arr) + increment; + int m = dbl_cur > min_needed ? dbl_cur : min_needed; + int *p = (int *) DMON_REALLOC(arr ? stb__sbraw(arr) : 0, itemsize * m + sizeof(int)*2); + if (p) { + if (!arr) + p[1] = 0; + p[0] = m; + return p+2; + } else { + return (void *) (2*sizeof(int)); // try to force a NULL pointer exception later + } +} + +// watcher callback (same as dmon.h's decleration) +typedef void (dmon__watch_cb)(dmon_watch_id, dmon_action, const char*, const char*, const char*, void*); + +#if DMON_OS_WINDOWS +// IOCP (windows) +#ifdef UNICODE +# define _DMON_WINAPI_STR(name, size) wchar_t _##name[size]; MultiByteToWideChar(CP_UTF8, 0, name, -1, _##name, size) +#else +# define _DMON_WINAPI_STR(name, size) const char* _##name = name +#endif + +typedef struct dmon__win32_event { + char filepath[DMON_MAX_PATH]; + DWORD action; + dmon_watch_id watch_id; + bool skip; +} dmon__win32_event; + +typedef struct dmon__watch_state { + dmon_watch_id id; + OVERLAPPED overlapped; + HANDLE dir_handle; + uint8_t buffer[64512]; // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365465(v=vs.85).aspx + DWORD notify_filter; + dmon__watch_cb* watch_cb; + uint32_t watch_flags; + void* user_data; + char rootdir[DMON_MAX_PATH]; + char old_filepath[DMON_MAX_PATH]; +} dmon__watch_state; + +typedef struct dmon__state { + int num_watches; + dmon__watch_state watches[DMON_MAX_WATCHES]; + HANDLE thread_handle; + CRITICAL_SECTION mutex; + volatile LONG modify_watches; + dmon__win32_event* events; + bool quit; +} dmon__state; + +static bool _dmon_init; +static dmon__state _dmon; + +_DMON_PRIVATE bool dmon__refresh_watch(dmon__watch_state* watch) +{ + return ReadDirectoryChangesW(watch->dir_handle, watch->buffer, sizeof(watch->buffer), + (watch->watch_flags & DMON_WATCHFLAGS_RECURSIVE) ? TRUE : FALSE, + watch->notify_filter, NULL, &watch->overlapped, NULL) != 0; +} + +_DMON_PRIVATE void dmon__unwatch(dmon__watch_state* watch) +{ + CancelIo(watch->dir_handle); + CloseHandle(watch->overlapped.hEvent); + CloseHandle(watch->dir_handle); + memset(watch, 0x0, sizeof(dmon__watch_state)); +} + +_DMON_PRIVATE void dmon__win32_process_events(void) +{ + for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__win32_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + + if (ev->action == FILE_ACTION_MODIFIED || ev->action == FILE_ACTION_ADDED) { + // remove duplicate modifies on a single file + for (int j = i + 1; j < c; j++) { + dmon__win32_event* check_ev = &_dmon.events[j]; + if (check_ev->action == FILE_ACTION_MODIFIED && + strcmp(ev->filepath, check_ev->filepath) == 0) { + check_ev->skip = true; + } + } + } + } + + // trigger user callbacks + for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__win32_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + dmon__watch_state* watch = &_dmon.watches[ev->watch_id.id - 1]; + + if(watch == NULL || watch->watch_cb == NULL) { + continue; + } + + switch (ev->action) { + case FILE_ACTION_ADDED: + watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, + watch->user_data); + break; + case FILE_ACTION_MODIFIED: + watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir, ev->filepath, NULL, + watch->user_data); + break; + case FILE_ACTION_RENAMED_OLD_NAME: { + // find the first occurance of the NEW_NAME + // this is somewhat API flaw that we have no reference for relating old and new files + for (int j = i + 1; j < c; j++) { + dmon__win32_event* check_ev = &_dmon.events[j]; + if (check_ev->action == FILE_ACTION_RENAMED_NEW_NAME) { + watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir, + check_ev->filepath, ev->filepath, watch->user_data); + break; + } + } + } break; + case FILE_ACTION_REMOVED: + watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir, ev->filepath, NULL, + watch->user_data); + break; + } + } + stb_sb_reset(_dmon.events); +} + +_DMON_PRIVATE DWORD WINAPI dmon__thread(LPVOID arg) +{ + _DMON_UNUSED(arg); + HANDLE wait_handles[DMON_MAX_WATCHES]; + + SYSTEMTIME starttm; + GetSystemTime(&starttm); + uint64_t msecs_elapsed = 0; + + while (!_dmon.quit) { + if (_dmon.modify_watches || !TryEnterCriticalSection(&_dmon.mutex)) { + Sleep(10); + continue; + } + + if (_dmon.num_watches == 0) { + Sleep(10); + LeaveCriticalSection(&_dmon.mutex); + continue; + } + + for (int i = 0; i < _dmon.num_watches; i++) { + dmon__watch_state* watch = &_dmon.watches[i]; + wait_handles[i] = watch->overlapped.hEvent; + } + + DWORD wait_result = WaitForMultipleObjects(_dmon.num_watches, wait_handles, FALSE, 10); + DMON_ASSERT(wait_result != WAIT_FAILED); + if (wait_result != WAIT_TIMEOUT) { + dmon__watch_state* watch = &_dmon.watches[wait_result - WAIT_OBJECT_0]; + DMON_ASSERT(HasOverlappedIoCompleted(&watch->overlapped)); + + DWORD bytes; + if (GetOverlappedResult(watch->dir_handle, &watch->overlapped, &bytes, FALSE)) { + char filepath[DMON_MAX_PATH]; + PFILE_NOTIFY_INFORMATION notify; + size_t offset = 0; + + if (bytes == 0) { + dmon__refresh_watch(watch); + LeaveCriticalSection(&_dmon.mutex); + continue; + } + + do { + notify = (PFILE_NOTIFY_INFORMATION)&watch->buffer[offset]; + + int count = WideCharToMultiByte(CP_UTF8, 0, notify->FileName, + notify->FileNameLength / sizeof(WCHAR), + filepath, DMON_MAX_PATH - 1, NULL, NULL); + filepath[count] = TEXT('\0'); + dmon__unixpath(filepath, sizeof(filepath), filepath); + + // TODO: ignore directories if flag is set + + if (stb_sb_count(_dmon.events) == 0) { + msecs_elapsed = 0; + } + dmon__win32_event wev = { { 0 }, notify->Action, watch->id, false }; + dmon__strcpy(wev.filepath, sizeof(wev.filepath), filepath); + stb_sb_push(_dmon.events, wev); + + offset += notify->NextEntryOffset; + } while (notify->NextEntryOffset > 0); + + if (!_dmon.quit) { + dmon__refresh_watch(watch); + } + } + } // if (WaitForMultipleObjects) + + SYSTEMTIME tm; + GetSystemTime(&tm); + LONG dt = + (tm.wSecond - starttm.wSecond) * 1000 + (tm.wMilliseconds - starttm.wMilliseconds); + starttm = tm; + msecs_elapsed += dt; + if (msecs_elapsed > 100 && stb_sb_count(_dmon.events) > 0) { + dmon__win32_process_events(); + msecs_elapsed = 0; + } + + LeaveCriticalSection(&_dmon.mutex); + } + return 0; +} + + +DMON_API_IMPL void dmon_init(void) +{ + DMON_ASSERT(!_dmon_init); + InitializeCriticalSection(&_dmon.mutex); + + _dmon.thread_handle = + CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)dmon__thread, NULL, 0, NULL); + DMON_ASSERT(_dmon.thread_handle); + _dmon_init = true; +} + + +DMON_API_IMPL void dmon_deinit(void) +{ + DMON_ASSERT(_dmon_init); + _dmon.quit = true; + if (_dmon.thread_handle != INVALID_HANDLE_VALUE) { + WaitForSingleObject(_dmon.thread_handle, INFINITE); + CloseHandle(_dmon.thread_handle); + } + + for (int i = 0; i < _dmon.num_watches; i++) { + dmon__unwatch(&_dmon.watches[i]); + } + + DeleteCriticalSection(&_dmon.mutex); + stb_sb_free(_dmon.events); + _dmon_init = false; +} + +DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, + void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, + const char* dirname, const char* filename, + const char* oldname, void* user), + uint32_t flags, void* user_data) +{ + DMON_ASSERT(watch_cb); + DMON_ASSERT(rootdir && rootdir[0]); + + _InterlockedExchange(&_dmon.modify_watches, 1); + EnterCriticalSection(&_dmon.mutex); + + DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES); + + uint32_t id = ++_dmon.num_watches; + dmon__watch_state* watch = &_dmon.watches[id - 1]; + watch->id = dmon__make_id(id); + watch->watch_flags = flags; + watch->watch_cb = watch_cb; + watch->user_data = user_data; + + dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir); + dmon__unixpath(watch->rootdir, sizeof(watch->rootdir), rootdir); + size_t rootdir_len = strlen(watch->rootdir); + if (watch->rootdir[rootdir_len - 1] != '/') { + watch->rootdir[rootdir_len] = '/'; + watch->rootdir[rootdir_len + 1] = '\0'; + } + + _DMON_WINAPI_STR(rootdir, DMON_MAX_PATH); + watch->dir_handle = + CreateFile(_rootdir, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL); + if (watch->dir_handle != INVALID_HANDLE_VALUE) { + watch->notify_filter = FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_LAST_WRITE | + FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | + FILE_NOTIFY_CHANGE_SIZE; + watch->overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + DMON_ASSERT(watch->overlapped.hEvent != INVALID_HANDLE_VALUE); + + if (!dmon__refresh_watch(watch)) { + dmon__unwatch(watch); + DMON_LOG_ERROR("ReadDirectoryChanges failed"); + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); + return dmon__make_id(0); + } + } else { + _DMON_LOG_ERRORF("Could not open: %s", rootdir); + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); + return dmon__make_id(0); + } + + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); + return dmon__make_id(id); +} + +DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) +{ + DMON_ASSERT(id.id > 0); + + _InterlockedExchange(&_dmon.modify_watches, 1); + EnterCriticalSection(&_dmon.mutex); + + int index = id.id - 1; + DMON_ASSERT(index < _dmon.num_watches); + + dmon__unwatch(&_dmon.watches[index]); + if (index != _dmon.num_watches - 1) { + dmon__swap(_dmon.watches[index], _dmon.watches[_dmon.num_watches - 1], dmon__watch_state); + } + --_dmon.num_watches; + + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); +} + +#elif DMON_OS_LINUX +// inotify linux backend +#define _DMON_TEMP_BUFFSIZE ((sizeof(struct inotify_event) + PATH_MAX) * 1024) + +typedef struct dmon__watch_subdir { + char rootdir[DMON_MAX_PATH]; +} dmon__watch_subdir; + +typedef struct dmon__inotify_event { + char filepath[DMON_MAX_PATH]; + uint32_t mask; + uint32_t cookie; + dmon_watch_id watch_id; + bool skip; +} dmon__inotify_event; + +typedef struct dmon__watch_state { + dmon_watch_id id; + int fd; + uint32_t watch_flags; + dmon__watch_cb* watch_cb; + void* user_data; + char rootdir[DMON_MAX_PATH]; + dmon__watch_subdir* subdirs; + int* wds; +} dmon__watch_state; + +typedef struct dmon__state { + dmon__watch_state watches[DMON_MAX_WATCHES]; + dmon__inotify_event* events; + int num_watches; + pthread_t thread_handle; + pthread_mutex_t mutex; + bool quit; +} dmon__state; + +static bool _dmon_init; +static dmon__state _dmon; + +_DMON_PRIVATE void dmon__watch_recursive(const char* dirname, int fd, uint32_t mask, + bool followlinks, dmon__watch_state* watch) +{ + struct dirent* entry; + DIR* dir = opendir(dirname); + DMON_ASSERT(dir); + + char watchdir[DMON_MAX_PATH]; + + while ((entry = readdir(dir)) != NULL) { + bool entry_valid = false; + if (entry->d_type == DT_DIR) { + if (strcmp(entry->d_name, "..") != 0 && strcmp(entry->d_name, ".") != 0) { + dmon__strcpy(watchdir, sizeof(watchdir), dirname); + dmon__strcat(watchdir, sizeof(watchdir), entry->d_name); + entry_valid = true; + } + } else if (followlinks && entry->d_type == DT_LNK) { + char linkpath[PATH_MAX]; + dmon__strcpy(watchdir, sizeof(watchdir), dirname); + dmon__strcat(watchdir, sizeof(watchdir), entry->d_name); + char* r = realpath(watchdir, linkpath); + _DMON_UNUSED(r); + DMON_ASSERT(r); + dmon__strcpy(watchdir, sizeof(watchdir), linkpath); + entry_valid = true; + } + + // add sub-directory to watch dirs + if (entry_valid) { + int watchdir_len = (int)strlen(watchdir); + if (watchdir[watchdir_len - 1] != '/') { + watchdir[watchdir_len] = '/'; + watchdir[watchdir_len + 1] = '\0'; + } + int wd = inotify_add_watch(fd, watchdir, mask); + _DMON_UNUSED(wd); + DMON_ASSERT(wd != -1); + + dmon__watch_subdir subdir; + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); + if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir)); + } + + stb_sb_push(watch->subdirs, subdir); + stb_sb_push(watch->wds, wd); + + // recurse + dmon__watch_recursive(watchdir, fd, mask, followlinks, watch); + } + } + closedir(dir); +} + +DMON_API_IMPL bool dmon_watch_add(dmon_watch_id id, const char* watchdir) +{ + DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES); + + bool skip_lock = pthread_self() == _dmon.thread_handle; + + if (!skip_lock) + pthread_mutex_lock(&_dmon.mutex); + + dmon__watch_state* watch = &_dmon.watches[id.id - 1]; + + // check if the directory exists + // if watchdir contains absolute/root-included path, try to strip the rootdir from it + // else, we assume that watchdir is correct, so save it as it is + struct stat st; + dmon__watch_subdir subdir; + if (stat(watchdir, &st) == 0 && (st.st_mode & S_IFDIR)) { + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); + if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir)); + } + } else { + char fullpath[DMON_MAX_PATH]; + dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir); + dmon__strcat(fullpath, sizeof(fullpath), watchdir); + if (stat(fullpath, &st) != 0 || (st.st_mode & S_IFDIR) == 0) { + _DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); + } + + int dirlen = (int)strlen(subdir.rootdir); + if (subdir.rootdir[dirlen - 1] != '/') { + subdir.rootdir[dirlen] = '/'; + subdir.rootdir[dirlen + 1] = '\0'; + } + + // check that the directory is not already added + for (int i = 0, c = stb_sb_count(watch->subdirs); i < c; i++) { + if (strcmp(subdir.rootdir, watch->subdirs[i].rootdir) == 0) { + _DMON_LOG_ERRORF("Error watching directory '%s', because it is already added.", watchdir); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + } + + const uint32_t inotify_mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; + char fullpath[DMON_MAX_PATH]; + dmon__strcpy(fullpath, sizeof(fullpath), watch->rootdir); + dmon__strcat(fullpath, sizeof(fullpath), subdir.rootdir); + int wd = inotify_add_watch(watch->fd, fullpath, inotify_mask); + if (wd == -1) { + _DMON_LOG_ERRORF("Error watching directory '%s'. (inotify_add_watch:err=%d)", watchdir, errno); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + + stb_sb_push(watch->subdirs, subdir); + stb_sb_push(watch->wds, wd); + + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + + return true; +} + +DMON_API_IMPL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir) +{ + DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES); + + bool skip_lock = pthread_self() == _dmon.thread_handle; + + if (!skip_lock) + pthread_mutex_lock(&_dmon.mutex); + + dmon__watch_state* watch = &_dmon.watches[id.id - 1]; + + char subdir[DMON_MAX_PATH]; + dmon__strcpy(subdir, sizeof(subdir), watchdir); + if (strstr(subdir, watch->rootdir) == subdir) { + dmon__strcpy(subdir, sizeof(subdir), watchdir + strlen(watch->rootdir)); + } + + int dirlen = (int)strlen(subdir); + if (subdir[dirlen - 1] != '/') { + subdir[dirlen] = '/'; + subdir[dirlen + 1] = '\0'; + } + + int i, c = stb_sb_count(watch->subdirs); + for (i = 0; i < c; i++) { + if (strcmp(watch->subdirs[i].rootdir, subdir) == 0) { + break; + } + } + if (i >= c) { + _DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + inotify_rm_watch(watch->fd, watch->wds[i]); + + for (int j = i; j < c - 1; j++) { + memcpy(watch->subdirs + j, watch->subdirs + j + 1, sizeof(dmon__watch_subdir)); + memcpy(watch->wds + j, watch->wds + j + 1, sizeof(int)); + } + stb__sbraw(watch->subdirs)[1] = c - 1; + stb__sbraw(watch->wds)[1] = c - 1; + + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return true; +} + +_DMON_PRIVATE const char* dmon__find_subdir(const dmon__watch_state* watch, int wd) +{ + const int* wds = watch->wds; + for (int i = 0, c = stb_sb_count(wds); i < c; i++) { + if (wd == wds[i]) { + return watch->subdirs[i].rootdir; + } + } + + return NULL; +} + +_DMON_PRIVATE void dmon__gather_recursive(dmon__watch_state* watch, const char* dirname) +{ + struct dirent* entry; + DIR* dir = opendir(dirname); + DMON_ASSERT(dir); + + char newdir[DMON_MAX_PATH]; + while ((entry = readdir(dir)) != NULL) { + bool entry_valid = false; + bool is_dir = false; + if (strcmp(entry->d_name, "..") != 0 && strcmp(entry->d_name, ".") != 0) { + dmon__strcpy(newdir, sizeof(newdir), dirname); + dmon__strcat(newdir, sizeof(newdir), entry->d_name); + is_dir = (entry->d_type == DT_DIR); + entry_valid = true; + } + + // add sub-directory to watch dirs + if (entry_valid) { + dmon__watch_subdir subdir; + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), newdir); + if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), newdir + strlen(watch->rootdir)); + } + + dmon__inotify_event dev = { { 0 }, IN_CREATE|(is_dir ? IN_ISDIR : 0), 0, watch->id, false }; + dmon__strcpy(dev.filepath, sizeof(dev.filepath), subdir.rootdir); + stb_sb_push(_dmon.events, dev); + } + } + closedir(dir); +} + +_DMON_PRIVATE void dmon__inotify_process_events(void) +{ + for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__inotify_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + + // remove redundant modify events on a single file + if (ev->mask & IN_MODIFY) { + for (int j = i + 1; j < c; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) { + ev->skip = true; + break; + } else if ((ev->mask & IN_ISDIR) && (check_ev->mask & (IN_ISDIR|IN_MODIFY))) { + // in some cases, particularly when created files under sub directories + // there can be two modify events for a single subdir one with trailing slash and one without + // remove traling slash from both cases and test + int l1 = (int)strlen(ev->filepath); + int l2 = (int)strlen(check_ev->filepath); + if (ev->filepath[l1-1] == '/') ev->filepath[l1-1] = '\0'; + if (check_ev->filepath[l2-1] == '/') check_ev->filepath[l2-1] = '\0'; + if (strcmp(ev->filepath, check_ev->filepath) == 0) { + ev->skip = true; + break; + } + } + } + } else if (ev->mask & IN_CREATE) { + bool loop_break = false; + for (int j = i + 1; j < c && !loop_break; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if ((check_ev->mask & IN_MOVED_FROM) && strcmp(ev->filepath, check_ev->filepath) == 0) { + // there is a case where some programs (like gedit): + // when we save, it creates a temp file, and moves it to the file being modified + // search for these cases and remove all of them + for (int k = j + 1; k < c; k++) { + dmon__inotify_event* third_ev = &_dmon.events[k]; + if (third_ev->mask & IN_MOVED_TO && check_ev->cookie == third_ev->cookie) { + third_ev->mask = IN_MODIFY; // change to modified + ev->skip = check_ev->skip = true; + loop_break = true; + break; + } + } + } else if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) { + // Another case is that file is copied. CREATE and MODIFY happens sequentially + // so we ignore MODIFY event + check_ev->skip = true; + } + } + } else if (ev->mask & IN_MOVED_FROM) { + bool move_valid = false; + for (int j = i + 1; j < c; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if (check_ev->mask & IN_MOVED_TO && ev->cookie == check_ev->cookie) { + move_valid = true; + break; + } + } + + // in some environments like nautilus file explorer: + // when a file is deleted, it is moved to recycle bin + // so if the destination of the move is not valid, it's probably DELETE + if (!move_valid) { + ev->mask = IN_DELETE; + } + } else if (ev->mask & IN_MOVED_TO) { + bool move_valid = false; + for (int j = 0; j < i; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if (check_ev->mask & IN_MOVED_FROM && ev->cookie == check_ev->cookie) { + move_valid = true; + break; + } + } + + // in some environments like nautilus file explorer: + // when a file is deleted, it is moved to recycle bin, on undo it is moved back it + // so if the destination of the move is not valid, it's probably CREATE + if (!move_valid) { + ev->mask = IN_CREATE; + } + } else if (ev->mask & IN_DELETE) { + for (int j = i + 1; j < c; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + // if the file is DELETED and then MODIFIED after, just ignore the modify event + if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) { + check_ev->skip = true; + break; + } + } + } + } + + // trigger user callbacks + for (int i = 0; i < stb_sb_count(_dmon.events); i++) { + dmon__inotify_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + dmon__watch_state* watch = &_dmon.watches[ev->watch_id.id - 1]; + + if(watch == NULL || watch->watch_cb == NULL) { + continue; + } + + if (ev->mask & IN_CREATE) { + if (ev->mask & IN_ISDIR) { + if (watch->watch_flags & DMON_WATCHFLAGS_RECURSIVE) { + char watchdir[DMON_MAX_PATH]; + dmon__strcpy(watchdir, sizeof(watchdir), watch->rootdir); + dmon__strcat(watchdir, sizeof(watchdir), ev->filepath); + dmon__strcat(watchdir, sizeof(watchdir), "/"); + uint32_t mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; + int wd = inotify_add_watch(watch->fd, watchdir, mask); + _DMON_UNUSED(wd); + DMON_ASSERT(wd != -1); + + dmon__watch_subdir subdir; + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); + if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir)); + } + + stb_sb_push(watch->subdirs, subdir); + stb_sb_push(watch->wds, wd); + + // some directories may be already created, for instance, with the command: mkdir -p + // so we will enumerate them manually and add them to the events + dmon__gather_recursive(watch, watchdir); + ev = &_dmon.events[i]; // gotta refresh the pointer because it may be relocated + } + } + watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, watch->user_data); + } + else if (ev->mask & IN_MODIFY) { + watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir, ev->filepath, NULL, watch->user_data); + } + else if (ev->mask & IN_MOVED_FROM) { + for (int j = i + 1; j < stb_sb_count(_dmon.events); j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if (check_ev->mask & IN_MOVED_TO && ev->cookie == check_ev->cookie) { + watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir, + check_ev->filepath, ev->filepath, watch->user_data); + break; + } + } + } + else if (ev->mask & IN_DELETE) { + watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir, ev->filepath, NULL, watch->user_data); + } + } + + stb_sb_reset(_dmon.events); +} + +static void* dmon__thread(void* arg) +{ + _DMON_UNUSED(arg); + + static uint8_t buff[_DMON_TEMP_BUFFSIZE]; + struct timespec req = { (time_t)10 / 1000, (long)(10 * 1000000) }; + struct timespec rem = { 0, 0 }; + struct timeval timeout; + uint64_t usecs_elapsed = 0; + + struct timeval starttm; + gettimeofday(&starttm, 0); + + while (!_dmon.quit) { + nanosleep(&req, &rem); + if (_dmon.num_watches == 0 || pthread_mutex_trylock(&_dmon.mutex) != 0) { + continue; + } + + // Create read FD set + fd_set rfds; + FD_ZERO(&rfds); + for (int i = 0; i < _dmon.num_watches; i++) { + dmon__watch_state* watch = &_dmon.watches[i]; + FD_SET(watch->fd, &rfds); + } + + timeout.tv_sec = 0; + timeout.tv_usec = 100000; + if (select(FD_SETSIZE, &rfds, NULL, NULL, &timeout)) { + for (int i = 0; i < _dmon.num_watches; i++) { + dmon__watch_state* watch = &_dmon.watches[i]; + if (FD_ISSET(watch->fd, &rfds)) { + ssize_t offset = 0; + ssize_t len = read(watch->fd, buff, _DMON_TEMP_BUFFSIZE); + if (len <= 0) { + continue; + } + + while (offset < len) { + struct inotify_event* iev = (struct inotify_event*)&buff[offset]; + + const char *subdir = dmon__find_subdir(watch, iev->wd); + if (subdir) { + char filepath[DMON_MAX_PATH]; + dmon__strcpy(filepath, sizeof(filepath), subdir); + dmon__strcat(filepath, sizeof(filepath), iev->name); + + // TODO: ignore directories if flag is set + + if (stb_sb_count(_dmon.events) == 0) { + usecs_elapsed = 0; + } + dmon__inotify_event dev = { { 0 }, iev->mask, iev->cookie, watch->id, false }; + dmon__strcpy(dev.filepath, sizeof(dev.filepath), filepath); + stb_sb_push(_dmon.events, dev); + } + + offset += sizeof(struct inotify_event) + iev->len; + } + } + } + } + + struct timeval tm; + gettimeofday(&tm, 0); + long dt = (tm.tv_sec - starttm.tv_sec) * 1000000 + tm.tv_usec - starttm.tv_usec; + starttm = tm; + usecs_elapsed += dt; + if (usecs_elapsed > 100000 && stb_sb_count(_dmon.events) > 0) { + dmon__inotify_process_events(); + usecs_elapsed = 0; + } + + pthread_mutex_unlock(&_dmon.mutex); + } + return 0x0; +} + +_DMON_PRIVATE void dmon__unwatch(dmon__watch_state* watch) +{ + close(watch->fd); + stb_sb_free(watch->subdirs); + stb_sb_free(watch->wds); + memset(watch, 0x0, sizeof(dmon__watch_state)); +} + +DMON_API_IMPL void dmon_init(void) +{ + DMON_ASSERT(!_dmon_init); + pthread_mutex_init(&_dmon.mutex, NULL); + + int r = pthread_create(&_dmon.thread_handle, NULL, dmon__thread, NULL); + _DMON_UNUSED(r); + DMON_ASSERT(r == 0 && "pthread_create failed"); + _dmon_init = true; +} + +DMON_API_IMPL void dmon_deinit(void) +{ + DMON_ASSERT(_dmon_init); + _dmon.quit = true; + pthread_join(_dmon.thread_handle, NULL); + + for (int i = 0; i < _dmon.num_watches; i++) { + dmon__unwatch(&_dmon.watches[i]); + } + + pthread_mutex_destroy(&_dmon.mutex); + stb_sb_free(_dmon.events); + _dmon_init = false; +} + +DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, + void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, + const char* dirname, const char* filename, + const char* oldname, void* user), + uint32_t flags, void* user_data) +{ + DMON_ASSERT(watch_cb); + DMON_ASSERT(rootdir && rootdir[0]); + + pthread_mutex_lock(&_dmon.mutex); + + DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES); + + uint32_t id = ++_dmon.num_watches; + dmon__watch_state* watch = &_dmon.watches[id - 1]; + watch->id = dmon__make_id(id); + watch->watch_flags = flags; + watch->watch_cb = watch_cb; + watch->user_data = user_data; + + struct stat root_st; + if (stat(rootdir, &root_st) != 0 || !S_ISDIR(root_st.st_mode) || + (root_st.st_mode & S_IRUSR) != S_IRUSR) { + _DMON_LOG_ERRORF("Could not open/read directory: %s", rootdir); + pthread_mutex_unlock(&_dmon.mutex); + return dmon__make_id(0); + } + + + if (S_ISLNK(root_st.st_mode)) { + if (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) { + char linkpath[PATH_MAX]; + char* r = realpath(rootdir, linkpath); + _DMON_UNUSED(r); + DMON_ASSERT(r); + + dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, linkpath); + } else { + _DMON_LOG_ERRORF("symlinks are unsupported: %s. use DMON_WATCHFLAGS_FOLLOW_SYMLINKS", + rootdir); + pthread_mutex_unlock(&_dmon.mutex); + return dmon__make_id(0); + } + } else { + dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir); + } + + // add trailing slash + int rootdir_len = (int)strlen(watch->rootdir); + if (watch->rootdir[rootdir_len - 1] != '/') { + watch->rootdir[rootdir_len] = '/'; + watch->rootdir[rootdir_len + 1] = '\0'; + } + + watch->fd = inotify_init(); + if (watch->fd < -1) { + DMON_LOG_ERROR("could not create inotify instance"); + pthread_mutex_unlock(&_dmon.mutex); + return dmon__make_id(0); + } + + uint32_t inotify_mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; + int wd = inotify_add_watch(watch->fd, watch->rootdir, inotify_mask); + if (wd < 0) { + _DMON_LOG_ERRORF("Error watching directory '%s'. (inotify_add_watch:err=%d)", watch->rootdir, errno); + pthread_mutex_unlock(&_dmon.mutex); + return dmon__make_id(0); + } + dmon__watch_subdir subdir; + dmon__strcpy(subdir.rootdir, sizeof(subdir.rootdir), ""); // root dir is just a dummy entry + stb_sb_push(watch->subdirs, subdir); + stb_sb_push(watch->wds, wd); + + // recursive mode: enumarate all child directories and add them to watch + if (flags & DMON_WATCHFLAGS_RECURSIVE) { + dmon__watch_recursive(watch->rootdir, watch->fd, inotify_mask, + (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) ? true : false, watch); + } + + + pthread_mutex_unlock(&_dmon.mutex); + return dmon__make_id(id); +} + +DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) +{ + DMON_ASSERT(id.id > 0); + + pthread_mutex_lock(&_dmon.mutex); + + int index = id.id - 1; + DMON_ASSERT(index < _dmon.num_watches); + + dmon__unwatch(&_dmon.watches[index]); + if (index != _dmon.num_watches - 1) { + dmon__swap(_dmon.watches[index], _dmon.watches[_dmon.num_watches - 1], dmon__watch_state); + } + --_dmon.num_watches; + + pthread_mutex_unlock(&_dmon.mutex); +} +#elif DMON_OS_MACOS +// FSEvents MacOS backend +typedef struct dmon__fsevent_event { + char filepath[DMON_MAX_PATH]; + uint64_t event_id; + long event_flags; + dmon_watch_id watch_id; + bool skip; + bool move_valid; +} dmon__fsevent_event; + +typedef struct dmon__watch_state { + dmon_watch_id id; + uint32_t watch_flags; + FSEventStreamRef fsev_stream_ref; + dmon__watch_cb* watch_cb; + void* user_data; + char rootdir[DMON_MAX_PATH]; + char rootdir_unmod[DMON_MAX_PATH]; + bool init; +} dmon__watch_state; + +typedef struct dmon__state { + dmon__watch_state watches[DMON_MAX_WATCHES]; + dmon__fsevent_event* events; + int num_watches; + volatile int modify_watches; + pthread_t thread_handle; + dispatch_semaphore_t thread_sem; + pthread_mutex_t mutex; + CFRunLoopRef cf_loop_ref; + CFAllocatorRef cf_alloc_ref; + bool quit; +} dmon__state; + +union dmon__cast_userdata { + void* ptr; + uint32_t id; +}; + +static bool _dmon_init; +static dmon__state _dmon; + +_DMON_PRIVATE void* dmon__cf_malloc(CFIndex size, CFOptionFlags hints, void* info) +{ + _DMON_UNUSED(hints); + _DMON_UNUSED(info); + return DMON_MALLOC(size); +} + +_DMON_PRIVATE void dmon__cf_free(void* ptr, void* info) +{ + _DMON_UNUSED(info); + DMON_FREE(ptr); +} + +_DMON_PRIVATE void* dmon__cf_realloc(void* ptr, CFIndex newsize, CFOptionFlags hints, void* info) +{ + _DMON_UNUSED(hints); + _DMON_UNUSED(info); + return DMON_REALLOC(ptr, (size_t)newsize); +} + +_DMON_PRIVATE void dmon__fsevent_process_events(void) +{ + for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__fsevent_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + + // remove redundant modify events on a single file + if (ev->event_flags & kFSEventStreamEventFlagItemModified) { + for (int j = i + 1; j < c; j++) { + dmon__fsevent_event* check_ev = &_dmon.events[j]; + if ((check_ev->event_flags & kFSEventStreamEventFlagItemModified) && + strcmp(ev->filepath, check_ev->filepath) == 0) { + ev->skip = true; + break; + } + } + } else if ((ev->event_flags & kFSEventStreamEventFlagItemRenamed) && !ev->move_valid) { + for (int j = i + 1; j < c; j++) { + dmon__fsevent_event* check_ev = &_dmon.events[j]; + if ((check_ev->event_flags & kFSEventStreamEventFlagItemRenamed) && + check_ev->event_id == (ev->event_id + 1)) { + ev->move_valid = check_ev->move_valid = true; + break; + } + } + + // in some environments like finder file explorer: + // when a file is deleted, it is moved to recycle bin + // so if the destination of the move is not valid, it's probably DELETE or CREATE + // decide CREATE if file exists + if (!ev->move_valid) { + ev->event_flags &= ~kFSEventStreamEventFlagItemRenamed; + + char abs_filepath[DMON_MAX_PATH]; + dmon__watch_state* watch = &_dmon.watches[ev->watch_id.id-1]; + dmon__strcpy(abs_filepath, sizeof(abs_filepath), watch->rootdir); + dmon__strcat(abs_filepath, sizeof(abs_filepath), ev->filepath); + + struct stat root_st; + if (stat(abs_filepath, &root_st) != 0) { + ev->event_flags |= kFSEventStreamEventFlagItemRemoved; + } else { + ev->event_flags |= kFSEventStreamEventFlagItemCreated; + } + } + } + } + + // trigger user callbacks + for (int i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__fsevent_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + dmon__watch_state* watch = &_dmon.watches[ev->watch_id.id - 1]; + + if(watch == NULL || watch->watch_cb == NULL) { + continue; + } + + if (ev->event_flags & kFSEventStreamEventFlagItemCreated) { + watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir_unmod, ev->filepath, NULL, + watch->user_data); + } else if (ev->event_flags & kFSEventStreamEventFlagItemModified) { + watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir_unmod, ev->filepath, NULL, + watch->user_data); + } else if (ev->event_flags & kFSEventStreamEventFlagItemRenamed) { + for (int j = i + 1; j < c; j++) { + dmon__fsevent_event* check_ev = &_dmon.events[j]; + if (check_ev->event_flags & kFSEventStreamEventFlagItemRenamed) { + watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir_unmod, + check_ev->filepath, ev->filepath, watch->user_data); + break; + } + } + } else if (ev->event_flags & kFSEventStreamEventFlagItemRemoved) { + watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir_unmod, ev->filepath, NULL, + watch->user_data); + } + } + + stb_sb_reset(_dmon.events); +} + +static void* dmon__thread(void* arg) +{ + _DMON_UNUSED(arg); + + struct timespec req = { (time_t)10 / 1000, (long)(10 * 1000000) }; + struct timespec rem = { 0, 0 }; + + _dmon.cf_loop_ref = CFRunLoopGetCurrent(); + dispatch_semaphore_signal(_dmon.thread_sem); + + while (!_dmon.quit) { + if (_dmon.modify_watches || pthread_mutex_trylock(&_dmon.mutex) != 0) { + nanosleep(&req, &rem); + continue; + } + + if (_dmon.num_watches == 0) { + nanosleep(&req, &rem); + pthread_mutex_unlock(&_dmon.mutex); + continue; + } + + for (int i = 0; i < _dmon.num_watches; i++) { + dmon__watch_state* watch = &_dmon.watches[i]; + if (!watch->init) { + DMON_ASSERT(watch->fsev_stream_ref); + FSEventStreamScheduleWithRunLoop(watch->fsev_stream_ref, _dmon.cf_loop_ref, + kCFRunLoopDefaultMode); + FSEventStreamStart(watch->fsev_stream_ref); + + watch->init = true; + } + } + + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, kCFRunLoopRunTimedOut); + dmon__fsevent_process_events(); + + pthread_mutex_unlock(&_dmon.mutex); + } + + CFRunLoopStop(_dmon.cf_loop_ref); + _dmon.cf_loop_ref = NULL; + return 0x0; +} + +_DMON_PRIVATE void dmon__unwatch(dmon__watch_state* watch) +{ + if (watch->fsev_stream_ref) { + FSEventStreamStop(watch->fsev_stream_ref); + FSEventStreamInvalidate(watch->fsev_stream_ref); + FSEventStreamRelease(watch->fsev_stream_ref); + watch->fsev_stream_ref = NULL; + } + + memset(watch, 0x0, sizeof(dmon__watch_state)); +} + +DMON_API_IMPL void dmon_init(void) +{ + DMON_ASSERT(!_dmon_init); + pthread_mutex_init(&_dmon.mutex, NULL); + + CFAllocatorContext cf_alloc_ctx = { 0 }; + cf_alloc_ctx.allocate = dmon__cf_malloc; + cf_alloc_ctx.deallocate = dmon__cf_free; + cf_alloc_ctx.reallocate = dmon__cf_realloc; + _dmon.cf_alloc_ref = CFAllocatorCreate(NULL, &cf_alloc_ctx); + + _dmon.thread_sem = dispatch_semaphore_create(0); + DMON_ASSERT(_dmon.thread_sem); + + int r = pthread_create(&_dmon.thread_handle, NULL, dmon__thread, NULL); + _DMON_UNUSED(r); + DMON_ASSERT(r == 0 && "pthread_create failed"); + + // wait for thread to initialize loop object + dispatch_semaphore_wait(_dmon.thread_sem, DISPATCH_TIME_FOREVER); + + _dmon_init = true; +} + +DMON_API_IMPL void dmon_deinit(void) +{ + DMON_ASSERT(_dmon_init); + _dmon.quit = true; + pthread_join(_dmon.thread_handle, NULL); + + dispatch_release(_dmon.thread_sem); + + for (int i = 0; i < _dmon.num_watches; i++) { + dmon__unwatch(&_dmon.watches[i]); + } + + pthread_mutex_destroy(&_dmon.mutex); + stb_sb_free(_dmon.events); + if (_dmon.cf_alloc_ref) { + CFRelease(_dmon.cf_alloc_ref); + } + + _dmon_init = false; +} + +_DMON_PRIVATE void dmon__fsevent_callback(ConstFSEventStreamRef stream_ref, void* user_data, + size_t num_events, void* event_paths, + const FSEventStreamEventFlags event_flags[], + const FSEventStreamEventId event_ids[]) +{ + _DMON_UNUSED(stream_ref); + + union dmon__cast_userdata _userdata; + _userdata.ptr = user_data; + dmon_watch_id watch_id = dmon__make_id(_userdata.id); + DMON_ASSERT(watch_id.id > 0); + dmon__watch_state* watch = &_dmon.watches[watch_id.id - 1]; + char abs_filepath[DMON_MAX_PATH]; + char abs_filepath_lower[DMON_MAX_PATH]; + + for (size_t i = 0; i < num_events; i++) { + const char* filepath = ((const char**)event_paths)[i]; + long flags = (long)event_flags[i]; + uint64_t event_id = (uint64_t)event_ids[i]; + dmon__fsevent_event ev; + memset(&ev, 0x0, sizeof(ev)); + + dmon__strcpy(abs_filepath, sizeof(abs_filepath), filepath); + dmon__unixpath(abs_filepath, sizeof(abs_filepath), abs_filepath); + + // normalize path, so it would be the same on both MacOS file-system types (case/nocase) + dmon__tolower(abs_filepath_lower, sizeof(abs_filepath), abs_filepath); + DMON_ASSERT(strstr(abs_filepath_lower, watch->rootdir) == abs_filepath_lower); + + // strip the root dir from the begining + dmon__strcpy(ev.filepath, sizeof(ev.filepath), abs_filepath + strlen(watch->rootdir)); + + ev.event_flags = flags; + ev.event_id = event_id; + ev.watch_id = watch_id; + stb_sb_push(_dmon.events, ev); + } +} + +DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, + void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, + const char* dirname, const char* filename, + const char* oldname, void* user), + uint32_t flags, void* user_data) +{ + DMON_ASSERT(watch_cb); + DMON_ASSERT(rootdir && rootdir[0]); + + __sync_lock_test_and_set(&_dmon.modify_watches, 1); + pthread_mutex_lock(&_dmon.mutex); + + DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES); + + uint32_t id = ++_dmon.num_watches; + dmon__watch_state* watch = &_dmon.watches[id - 1]; + watch->id = dmon__make_id(id); + watch->watch_flags = flags; + watch->watch_cb = watch_cb; + watch->user_data = user_data; + + struct stat root_st; + if (stat(rootdir, &root_st) != 0 || !S_ISDIR(root_st.st_mode) || + (root_st.st_mode & S_IRUSR) != S_IRUSR) { + _DMON_LOG_ERRORF("Could not open/read directory: %s", rootdir); + pthread_mutex_unlock(&_dmon.mutex); + __sync_lock_test_and_set(&_dmon.modify_watches, 0); + return dmon__make_id(0); + } + + if (S_ISLNK(root_st.st_mode)) { + if (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) { + char linkpath[PATH_MAX]; + char* r = realpath(rootdir, linkpath); + _DMON_UNUSED(r); + DMON_ASSERT(r); + + dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, linkpath); + } else { + _DMON_LOG_ERRORF("symlinks are unsupported: %s. use DMON_WATCHFLAGS_FOLLOW_SYMLINKS", rootdir); + pthread_mutex_unlock(&_dmon.mutex); + __sync_lock_test_and_set(&_dmon.modify_watches, 0); + return dmon__make_id(0); + } + } else { + char rootdir_abspath[DMON_MAX_PATH]; + if (realpath(rootdir, rootdir_abspath) != NULL) { + dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir_abspath); + } else { + dmon__strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir); + } + } + + dmon__unixpath(watch->rootdir, sizeof(watch->rootdir), watch->rootdir); + + // add trailing slash + int rootdir_len = (int)strlen(watch->rootdir); + if (watch->rootdir[rootdir_len - 1] != '/') { + watch->rootdir[rootdir_len] = '/'; + watch->rootdir[rootdir_len + 1] = '\0'; + } + + dmon__strcpy(watch->rootdir_unmod, sizeof(watch->rootdir_unmod), watch->rootdir); + dmon__tolower(watch->rootdir, sizeof(watch->rootdir), watch->rootdir); + + // create FS objects + CFStringRef cf_dir = CFStringCreateWithCString(NULL, watch->rootdir_unmod, kCFStringEncodingUTF8); + CFArrayRef cf_dirarr = CFArrayCreate(NULL, (const void**)&cf_dir, 1, NULL); + + FSEventStreamContext ctx; + union dmon__cast_userdata userdata; + userdata.id = id; + ctx.version = 0; + ctx.info = userdata.ptr; + ctx.retain = NULL; + ctx.release = NULL; + ctx.copyDescription = NULL; + watch->fsev_stream_ref = FSEventStreamCreate(_dmon.cf_alloc_ref, dmon__fsevent_callback, &ctx, + cf_dirarr, kFSEventStreamEventIdSinceNow, 0.25, + kFSEventStreamCreateFlagFileEvents); + + + CFRelease(cf_dirarr); + CFRelease(cf_dir); + + pthread_mutex_unlock(&_dmon.mutex); + __sync_lock_test_and_set(&_dmon.modify_watches, 0); + return dmon__make_id(id); +} + +DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) +{ + DMON_ASSERT(id.id > 0); + + __sync_lock_test_and_set(&_dmon.modify_watches, 1); + pthread_mutex_lock(&_dmon.mutex); + + int index = id.id - 1; + DMON_ASSERT(index < _dmon.num_watches); + + dmon__unwatch(&_dmon.watches[index]); + if (index != _dmon.num_watches - 1) { + dmon__swap(_dmon.watches[index], _dmon.watches[_dmon.num_watches - 1], dmon__watch_state); + } + --_dmon.num_watches; + + pthread_mutex_unlock(&_dmon.mutex); + __sync_lock_test_and_set(&_dmon.modify_watches, 0); +} + +#endif + +#endif // DMON_IMPL +#endif // __DMON_H__ diff --git a/src/main.c b/src/main.c index 182511d0..e0fcb107 100644 --- a/src/main.c +++ b/src/main.c @@ -14,6 +14,8 @@ #include #endif +#include "dirmonitor.h" + SDL_Window *window; @@ -107,6 +109,8 @@ int main(int argc, char **argv) { SDL_DisplayMode dm; SDL_GetCurrentDisplayMode(0, &dm); + dirmonitor_init(); + window = SDL_CreateWindow( "", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, dm.w * 0.8, dm.h * 0.8, SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN); @@ -189,6 +193,7 @@ init_lua: lua_close(L); ren_free_window_resources(); + dirmonitor_deinit(); return EXIT_SUCCESS; } diff --git a/src/meson.build b/src/meson.build index 707e04e9..2da04fda 100644 --- a/src/meson.build +++ b/src/meson.build @@ -6,6 +6,7 @@ lite_sources = [ 'api/regex.c', 'api/system.c', 'api/process.c', + 'dirmonitor.c', 'renderer.c', 'renwindow.c', 'fontdesc.c',