Applied PR746

This commit is contained in:
George Sokianos 2022-02-05 15:50:39 +00:00
parent b5831ace20
commit 9d13525af0
15 changed files with 497 additions and 2168 deletions

View File

@ -6,7 +6,7 @@ config.message_timeout = 5
config.mouse_wheel_scroll = 50 * SCALE config.mouse_wheel_scroll = 50 * SCALE
config.scroll_past_end = true config.scroll_past_end = true
config.file_size_limit = 10 config.file_size_limit = 10
config.ignore_files = "^%." config.ignore_files = {"^%."}
config.symbol_pattern = "[%a_][%w_]*" config.symbol_pattern = "[%a_][%w_]*"
config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-" config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"
config.undo_merge_timeout = 0.3 config.undo_merge_timeout = 0.3

109
data/core/dirwatch.lua Normal file
View File

@ -0,0 +1,109 @@
local common = require "core.common"
local dirwatch = {}
function dirwatch:__index(idx)
local value = rawget(self, idx)
if value ~= nil then return value end
return dirwatch[idx]
end
function dirwatch.new()
local t = {
scanned = {},
watched = {},
reverse_watched = {},
monitor = dirmonitor.new(),
windows_watch_top = nil,
windows_watch_count = 0
}
setmetatable(t, dirwatch)
return t
end
function dirwatch:scan(directory, bool)
if bool == false then return self:unwatch(directory) end
self.scanned[directory] = system.get_file_info(directory).modified
end
-- Should be called on every directory in a subdirectory.
-- In windows, this is a no-op for anything underneath a top-level directory,
-- but code should be called anyway, so we can ensure that we have a proper
-- experience across all platforms.
function dirwatch:watch(directory, bool)
if bool == false then return self:unwatch(directory) end
if not self.watched[directory] and not self.scanned[directory] then
if PLATFORM == "Windows" then
if not self.windows_watch_top or directory:find(self.windows_watch_top, 1, true) ~= 1 then
-- Get the highest level of directory that is common to this directory, and the original.
local target = directory
while self.windows_watch_top and self.windows_watch_top:find(target, 1, true) ~= 1 do
target = common.dirname(target)
end
if target ~= self.windows_watch_top then
local value = self.monitor:watch(target)
if value and value < 0 then
return self:scan(directory)
end
self.windows_watch_top = target
self.windows_watch_count = self.windows_watch_count + 1
end
end
self.watched[directory] = true
else
local value = self.monitor:watch(directory)
-- If for whatever reason, we can't watch this directory, revert back to scanning.
-- Don't bother trying to find out why, for now.
if value and value < 0 then
return self:scan(directory)
end
self.watched[directory] = value
self.reverse_watched[value] = directory
end
end
end
function dirwatch:unwatch(directory)
if self.watched[directory] then
if PLATFORM ~= "Windows" then
self.monitor.unwatch(directory)
self.reverse_watched[self.watched[directory]] = nil
else
self.windows_watch_count = self.windows_watch_count - 1
if self.windows_watch_count == 0 then
self.windows_watch_top = nil
self.monitor.unwatch(directory)
end
end
self.watched[directory] = nil
elseif self.scanned[directory] then
self.scanned[directory] = nil
end
end
-- designed to be run inside a coroutine.
function dirwatch:check(change_callback, scan_time, wait_time)
self.monitor:check(function(id)
if PLATFORM == "Windows" then
change_callback(common.dirname(self.windows_watch_top .. PATHSEP .. id))
elseif self.reverse_watched[id] then
change_callback(self.reverse_watched[id])
end
end)
local start_time = system.get_time()
for directory, old_modified in pairs(self.scanned) do
if old_modified then
local new_modified = system.get_file_info(directory).modified
if old_modified < new_modified then
change_callback(directory)
self.scanned[directory] = new_modified
end
end
if system.get_time() - start_time > scan_time then
coroutine.yield(wait_time)
start_time = system.get_time()
end
end
end
return dirwatch

View File

@ -5,6 +5,7 @@ local config = require "core.config"
local style = require "core.style" local style = require "core.style"
local command local command
local keymap local keymap
local dirwatch
local RootView local RootView
local StatusView local StatusView
local TitleView local TitleView
@ -106,70 +107,8 @@ local function get_project_file_info(root, file)
end end
-- Predicate function to inhibit directory recursion in get_directory_files
-- based on a time limit and the number of files.
local function timed_max_files_pred(dir, filename, entries_count, t_elapsed)
local n_limit = entries_count <= config.max_project_files
local t_limit = t_elapsed < 20 / config.fps
return n_limit and t_limit and core.project_subdir_is_shown(dir, filename)
end
-- "root" will by an absolute path without trailing '/'
-- "path" will be a path starting with '/' and without trailing '/'
-- or the empty string.
-- It will identifies a sub-path within "root.
-- The current path location will therefore always be: root .. path.
-- 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(dir, root, path, t, entries_count, recurse_pred, begin_hook)
if begin_hook then begin_hook() end
local t0 = system.get_time()
local all = system.list_dir(root .. path) or {}
local t_elapsed = system.get_time() - t0
local dirs, files = {}, {}
for _, file in ipairs(all) do
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
local recurse_complete = true
table.sort(dirs, compare_file)
for _, f in ipairs(dirs) do
table.insert(t, f)
if recurse_pred(dir, f.filename, entries_count, t_elapsed) then
local _, complete, n = get_directory_files(dir, root, PATHSEP .. f.filename, t, entries_count, recurse_pred, begin_hook)
recurse_complete = recurse_complete and complete
entries_count = n
else
recurse_complete = false
end
end
table.sort(files, compare_file)
for _, f in ipairs(files) do
table.insert(t, f)
end
return t, recurse_complete, entries_count
end
function core.project_subdir_set_show(dir, filename, show) function core.project_subdir_set_show(dir, filename, show)
dir.shown_subdir[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
end
end end
@ -183,8 +122,10 @@ local function show_max_files_warning(dir)
"Filesystem is too slow: project files will not be indexed." or "Filesystem is too slow: project files will not be indexed." or
"Too many files in project directory: stopped reading at ".. "Too many files in project directory: stopped reading at "..
config.max_project_files.." files. For more information see ".. config.max_project_files.." files. For more information see "..
"usage.md at github.com/lite-xl/lite-xl." "usage.md at https://github.com/lite-xl/lite-xl."
if core.status_view then
core.status_view:show_message("!", style.accent, message) core.status_view:show_message("!", style.accent, message)
end
end end
@ -270,64 +211,129 @@ local function project_subdir_bounds(dir, filename)
end end
end end
local function rescan_project_subdir(dir, filename_rooted) -- Predicate function to inhibit directory recursion in get_directory_files
local new_files = get_directory_files(dir, dir.name, filename_rooted, {}, 0, core.project_subdir_is_shown, coroutine.yield) -- based on a time limit and the number of files.
local index, n = 0, #dir.files local function timed_max_files_pred(dir, filename, entries_count, t_elapsed)
if filename_rooted ~= "" then local n_limit = entries_count <= config.max_project_files
local filename = strip_leading_path(filename_rooted) local t_limit = t_elapsed < 20 / config.fps
index, n = project_subdir_bounds(dir, filename) return n_limit and t_limit and core.project_subdir_is_shown(dir, filename)
end
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 end
local function add_dir_scan_thread(dir) -- "root" will by an absolute path without trailing '/'
core.add_thread(function() -- "path" will be a path starting with '/' and without trailing '/'
while true do -- or the empty string.
local has_changes = rescan_project_subdir(dir, "") -- It will identifies a sub-path within "root.
if has_changes then -- The current path location will therefore always be: root .. path.
core.redraw = true -- we run without an event, from a thread -- 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(dir, root, path, t, entries_count, recurse_pred)
local t0 = system.get_time()
local all = system.list_dir(root .. path) or {}
local t_elapsed = system.get_time() - t0
local dirs, files = {}, {}
for _, file in ipairs(all) do
local info = get_project_file_info(root, path .. PATHSEP .. file)
if info and not common.match_pattern(common.basename(info.filename), config.ignore_files) then
table.insert(info.type == "dir" and dirs or files, info)
entries_count = entries_count + 1
end end
coroutine.yield(5)
end end
end)
local recurse_complete = true
table.sort(dirs, compare_file)
for _, f in ipairs(dirs) do
table.insert(t, f)
if recurse_pred(dir, f.filename, entries_count, t_elapsed) then
local _, complete, n = get_directory_files(dir, root, PATHSEP .. f.filename, t, entries_count, recurse_pred)
recurse_complete = recurse_complete and complete
entries_count = n
else
recurse_complete = false
end
end
table.sort(files, compare_file)
for _, f in ipairs(files) do
table.insert(t, f)
end
return t, recurse_complete, entries_count
end end
-- Populate a project folder top directory by scanning the filesystem.
local function scan_project_folder(index) -- Should be called on any directory that registers a change.
local dir = core.project_directories[index] -- Uses relative paths at the project root.
if PLATFORM == "Linux" then local function refresh_directory(topdir, target, expanded)
local fstype = system.get_fs_type(dir.name) local index, n, directory
dir.force_rescan = (fstype == "nfs" or fstype == "fuse") if target == "" then
index, n = 1, #topdir.files
directory = ""
else
index, n = project_subdir_bounds(topdir, target)
index = index + 1
n = index + n - 1
directory = (PATHSEP .. target)
end end
local t, complete, entries_count = get_directory_files(dir, dir.name, "", {}, 0, timed_max_files_pred) if index then
if not complete then local files
dir.slow_filesystem = not complete and (entries_count <= config.max_project_files) local change = false
dir.files_limit = true if topdir.files_limit then
if not dir.force_rescan then -- If we have the folders literally open on the side panel.
-- Watch non-recursively on Linux only. files = expanded and get_directory_files(topdir, topdir.name, directory, {}, 0, core.project_subdir_is_shown) or {}
-- The reason is recursively watching with dmon on linux change = true
-- doesn't work on very large directories. else
dir.watch_id = system.watch_dir(dir.name, PLATFORM ~= "Linux") -- If we're expecting to keep track of everything, go through the list and iteratively deal with directories.
files = get_directory_files(topdir, topdir.name, directory, {}, 0, function() return false end)
end end
if core.status_view then -- May be not yet initialized.
show_max_files_warning(dir) local new_idx, old_idx = 1, index
local new_directories = {}
local last_dir = nil
while old_idx <= n or new_idx <= #files do
local old_info, new_info = topdir.files[old_idx], files[new_idx]
if not new_info or not old_info or not last_dir or old_info.filename:sub(1, #last_dir + 1) ~= last_dir .. "/" then
if not new_info or not old_info or not files_info_equal(new_info, old_info) then
change = true
if not old_info or (new_info and system.path_compare(new_info.filename, new_info.type, old_info.filename, old_info.type)) then
table.insert(topdir.files, old_idx, new_info)
new_idx = new_idx + 1
old_idx = old_idx + 1
if new_info.type == "dir" then
table.insert(new_directories, new_info)
end
n = n + 1
else
table.remove(topdir.files, old_idx)
if old_info.type == "dir" then
topdir.watch:unwatch(target .. PATHSEP .. old_info.filename)
end
n = n - 1
end end
else else
if not dir.force_rescan then new_idx = new_idx + 1
dir.watch_id = system.watch_dir(dir.name, true) old_idx = old_idx + 1
end end
if old_info and old_info.type == "dir" then
last_dir = old_info.filename
end end
dir.files = t
if dir.force_rescan then
add_dir_scan_thread(dir)
else else
core.dir_rescan_add_job(dir, ".") old_idx = old_idx + 1
end
end
for i, v in ipairs(new_directories) do
topdir.watch:watch(target)
if refresh_directory(topdir, target .. PATHSEP .. v.filename) then
change = true
end
end
if change then
core.redraw = true
topdir.is_dirty = true
end
return change
end end
end end
@ -337,29 +343,63 @@ function core.add_project_directory(path)
-- will be simply the name of the directory, without its path. -- will be simply the name of the directory, without its path.
-- The field item.topdir will identify it as a top level directory. -- The field item.topdir will identify it as a top level directory.
path = common.normalize_volume(path) path = common.normalize_volume(path)
local dir = { local topdir = {
name = path, name = path,
item = {filename = common.basename(path), type = "dir", topdir = true}, item = {filename = common.basename(path), type = "dir", topdir = true},
files_limit = false, files_limit = false,
is_dirty = true, is_dirty = true,
shown_subdir = {}, shown_subdir = {},
watch_thread = nil,
watch = dirwatch.new()
} }
table.insert(core.project_directories, dir) table.insert(core.project_directories, topdir)
scan_project_folder(#core.project_directories)
local fstype = PLATFORM == "Linux" and system.get_fs_type(topdir.name) or "unknown"
topdir.force_scans = (fstype == "nfs" or fstype == "fuse")
local t, complete, entries_count = get_directory_files(topdir, topdir.name, "", {}, 0, timed_max_files_pred)
topdir.files = t
if not complete then
topdir.slow_filesystem = not complete and (entries_count <= config.max_project_files)
topdir.files_limit = true
show_max_files_warning(topdir)
refresh_directory(topdir, "", true)
else
for i,v in ipairs(t) do
if v.type == "dir" then topdir.watch:watch(path .. PATHSEP .. v.filename) end
end
end
topdir.watch:watch(topdir.name)
-- each top level directory gets a watch thread. if the project is small, or
-- if the ablity to use directory watches hasn't been compromised in some way
-- either through error, or amount of files, then this should be incredibly
-- quick; essentially one syscall per check. Otherwise, this may take a bit of
-- time; the watch will yield in this coroutine after 0.01 second, for 0.1 seconds.
topdir.watch_thread = core.add_thread(function()
while true do
topdir.watch:check(function(target)
if target == topdir.name then return refresh_directory(topdir, "", true) end
local dirpath = target:sub(#topdir.name + 2)
local abs_dirpath = topdir.name .. PATHSEP .. dirpath
if dirpath then
-- check if the directory is in the project files list, if not exit.
local dir_index, dir_match = file_search(topdir.files, {filename = dirpath, type = "dir"})
if not dir_match or not core.project_subdir_is_shown(topdir, topdir.files[dir_index].filename) then return end
end
return refresh_directory(topdir, dirpath, true)
end, 0.01, 0.01)
coroutine.yield(0.05)
end
end)
if path == core.project_dir then if path == core.project_dir then
core.project_files = dir.files core.project_files = topdir.files
end end
core.redraw = true core.redraw = true
end end
function core.update_project_subdir(dir, filename, expanded) function core.update_project_subdir(dir, filename, expanded)
local index, n, file = project_subdir_bounds(dir, filename) if dir.files_limit then
if index then refresh_directory(dir, filename, expanded)
local new_files = expanded and get_directory_files(dir, dir.name, PATHSEP .. filename, {}, 0, core.project_subdir_is_shown) or {}
files_list_replace(dir.files, index, n, new_files)
dir.is_dirty = true
return true
end end
end end
@ -376,7 +416,7 @@ local function find_files_rec(root, path)
info.filename = strip_leading_path(file) info.filename = strip_leading_path(file)
if info.type == "file" then if info.type == "file" then
coroutine.yield(root, info) coroutine.yield(root, info)
else elseif not common.match_pattern(common.basename(info.filename), config.ignore_files) then
find_files_rec(root, PATHSEP .. info.filename) find_files_rec(root, PATHSEP .. info.filename)
end end
end end
@ -437,42 +477,6 @@ function core.project_files_number()
end 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
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
-- create a directory using mkdir but may need to create the parent -- create a directory using mkdir but may need to create the parent
-- directories as well. -- directories as well.
local function create_user_directory() local function create_user_directory()
@ -594,6 +598,7 @@ end
function core.init() function core.init()
command = require "core.command" command = require "core.command"
keymap = require "core.keymap" keymap = require "core.keymap"
dirwatch = require "core.dirwatch"
RootView = require "core.rootview" RootView = require "core.rootview"
StatusView = require "core.statusview" StatusView = require "core.statusview"
TitleView = require "core.titleview" TitleView = require "core.titleview"
@ -653,6 +658,8 @@ function core.init()
core.blink_start = system.get_time() core.blink_start = system.get_time()
core.blink_timer = core.blink_start core.blink_timer = core.blink_start
local got_user_error, got_project_error = not core.load_user_directory()
local project_dir_abs = system.absolute_path(project_dir) local project_dir_abs = system.absolute_path(project_dir)
local set_project_ok = project_dir_abs and core.set_project_dir(project_dir_abs) local set_project_ok = project_dir_abs and core.set_project_dir(project_dir_abs)
if set_project_ok then if set_project_ok then
@ -664,7 +671,9 @@ function core.init()
update_recents_project("remove", project_dir) update_recents_project("remove", project_dir)
end end
project_dir_abs = system.absolute_path(".") project_dir_abs = system.absolute_path(".")
if not core.set_project_dir(project_dir_abs) then if not core.set_project_dir(project_dir_abs, function()
got_project_error = not core.load_project_module()
end) then
system.show_fatal_error("Lite XL internal error", "cannot set project directory to cwd") system.show_fatal_error("Lite XL internal error", "cannot set project directory to cwd")
os.exit(1) os.exit(1)
end end
@ -691,14 +700,12 @@ function core.init()
cur_node = cur_node:split("down", core.status_view, {y = true}) cur_node = cur_node:split("down", core.status_view, {y = true})
command.add_defaults() command.add_defaults()
local got_user_error = not core.load_user_directory()
local plugins_success, plugins_refuse_list = core.load_plugins() local plugins_success, plugins_refuse_list = core.load_plugins()
do do
local pdir, pname = project_dir_abs:match("(.*)[:/\\\\](.*)") local pdir, pname = project_dir_abs:match("(.*)[:/\\\\](.*)")
core.log("Opening project %q from directory %s", pname, pdir) core.log("Opening project %q from directory %s", pname, pdir)
end end
local got_project_error = not core.load_project_module()
-- We assume we have just a single project directory here. Now that StatusView -- We assume we have just a single project directory here. Now that StatusView
-- is there show max files warning if needed. -- is there show max files warning if needed.
@ -1105,76 +1112,6 @@ function core.has_pending_rescan()
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
-- no-op but can be overrided by plugins
function core.on_dirmonitor_modify(dir, filepath)
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)
core.on_dirmonitor_modify(dir, filepath);
elseif action == "modify" then
core.on_dirmonitor_modify(dir, filepath);
end
end
function core.on_event(type, ...) function core.on_event(type, ...)
local did_keymap = false local did_keymap = false
if type == "textinput" then if type == "textinput" then
@ -1214,8 +1151,6 @@ function core.on_event(type, ...)
end end
elseif type == "focuslost" then elseif type == "focuslost" then
core.root_view:on_focus_lost(...) core.root_view:on_focus_lost(...)
elseif type == "dirchange" then
core.on_dir_change(...)
elseif type == "quit" then elseif type == "quit" then
core.quit() core.quit()
end end

View File

@ -577,7 +577,7 @@ command.add(function() return view.hovered_item ~= nil end, {
system.exec(string.format("start \"\" %q", hovered_item.abs_filename)) system.exec(string.format("start \"\" %q", hovered_item.abs_filename))
elseif string.find(PLATFORM, "Mac") then elseif string.find(PLATFORM, "Mac") then
system.exec(string.format("open %q", hovered_item.abs_filename)) system.exec(string.format("open %q", hovered_item.abs_filename))
elseif PLATFORM == "Linux" then elseif PLATFORM == "Linux" or string.find(PLATFORM, "BSD") then
system.exec(string.format("xdg-open %q", hovered_item.abs_filename)) system.exec(string.format("xdg-open %q", hovered_item.abs_filename))
end end
end, end,

File diff suppressed because it is too large Load Diff

View File

@ -1,162 +0,0 @@
#ifndef __DMON_EXTRA_H__
#define __DMON_EXTRA_H__
//
// Copyright 2021 Sepehr Taghdisian (septag@github). All rights reserved.
// License: https://github.com/septag/dmon#license-bsd-2-clause
//
// Extra header functionality for dmon.h for the backend based on inotify
//
// Add/Remove directory functions:
// dmon_watch_add: Adds a sub-directory to already valid watch_id. sub-directories are assumed to be relative to watch root_dir
// dmon_watch_add: Removes a sub-directory from already valid watch_id. sub-directories are assumed to be relative to watch root_dir
// Reason: The inotify backend does not work well when watching in recursive mode a root directory of a large tree, it may take
// quite a while until all inotify watches are established, and events will not be received in this time. Also, since one
// inotify watch will be established per subdirectory, it is possible that the maximum amount of inotify watches per user
// will be reached. The default maximum is 8192.
// When using inotify backend users may turn off the DMON_WATCHFLAGS_RECURSIVE flag and add/remove selectively the
// sub-directories to be watched based on application-specific logic about which sub-directory actually needs to be watched.
// The function dmon_watch_add and dmon_watch_rm are used to this purpose.
//
#ifndef __DMON_H__
#error "Include 'dmon.h' before including this file"
#endif
#ifdef __cplusplus
extern "C" {
#endif
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
#if DMON_OS_LINUX
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]);
/* Remove entry from subdirs and wds by swapping position with the last entry */
watch->subdirs[i] = stb_sb_last(watch->subdirs);
stb_sb_pop(watch->subdirs);
watch->wds[i] = stb_sb_last(watch->wds);
stb_sb_pop(watch->wds);
if (!skip_lock)
pthread_mutex_unlock(&_dmon.mutex);
return true;
}
#endif // DMON_OS_LINUX
#endif // DMON_IMPL
#endif // __DMON_EXTRA_H__

View File

@ -1 +0,0 @@
lite_includes += include_directories('.')

View File

@ -22,33 +22,6 @@ 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. 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 ## Fira Sans
Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A.

View File

@ -1,16 +1,17 @@
#include "api.h" #include "api.h"
int luaopen_system(lua_State *L); int luaopen_system(lua_State *L);
int luaopen_renderer(lua_State *L); int luaopen_renderer(lua_State *L);
int luaopen_regex(lua_State *L); int luaopen_regex(lua_State *L);
// int luaopen_process(lua_State *L); // int luaopen_process(lua_State *L);
int luaopen_dirmonitor(lua_State* L);
static const luaL_Reg libs[] = { static const luaL_Reg libs[] = {
{ "system", luaopen_system }, { "system", luaopen_system },
{ "renderer", luaopen_renderer }, { "renderer", luaopen_renderer },
{ "regex", luaopen_regex }, { "regex", luaopen_regex },
// { "process", luaopen_process }, // { "process", luaopen_process },
{ "dirmonitor", luaopen_dirmonitor },
{ NULL, NULL } { NULL, NULL }
}; };
@ -18,4 +19,3 @@ void api_load_libs(lua_State *L) {
for (int i = 0; libs[i].name; i++) for (int i = 0; libs[i].name; i++)
luaL_requiref(L, libs[i].name, libs[i].func, 1); luaL_requiref(L, libs[i].name, libs[i].func, 1);
} }

View File

@ -7,6 +7,7 @@
#define API_TYPE_FONT "Font" #define API_TYPE_FONT "Font"
#define API_TYPE_PROCESS "Process" #define API_TYPE_PROCESS "Process"
#define API_TYPE_DIRMONITOR "Dirmonitor"
#define API_CONSTANT_DEFINE(L, idx, key, n) (lua_pushnumber(L, n), lua_setfield(L, idx - 1, key)) #define API_CONSTANT_DEFINE(L, idx, key, n) (lua_pushnumber(L, n), lua_setfield(L, idx - 1, key))

202
src/api/dirmonitor.c Normal file
View File

@ -0,0 +1,202 @@
#include "api.h"
#include <stdlib.h>
#ifdef _WIN32
#include <windows.h>
#elif __linux__
#include <sys/inotify.h>
#include <limits.h>
#else
#include <sys/event.h>
#endif
#include <unistd.h>
#include <errno.h>
#include <dirent.h>
#include <fcntl.h>
#include <string.h>
#include <stdbool.h>
/*
This is *slightly* a clusterfuck. Normally, we'd
have windows wait on a list of handles like inotify,
however, MAXIMUM_WAIT_OBJECTS is 64. Yes, seriously.
So, for windows, we are recursive.
*/
struct dirmonitor {
int fd;
#if _WIN32
HANDLE handle;
char buffer[8192];
OVERLAPPED overlapped;
bool running;
#endif
};
struct dirmonitor* init_dirmonitor() {
struct dirmonitor* monitor = calloc(sizeof(struct dirmonitor), 1);
#ifndef _WIN32
#if __linux__
monitor->fd = inotify_init1(IN_NONBLOCK);
#else
monitor->fd = kqueue();
#endif
#endif
return monitor;
}
#if _WIN32
static void close_monitor_handle(struct dirmonitor* monitor) {
if (monitor->handle) {
if (monitor->running) {
BOOL result = CancelIoEx(monitor->handle, &monitor->overlapped);
DWORD error = GetLastError();
if (result == TRUE || error != ERROR_NOT_FOUND) {
DWORD bytes_transferred;
GetOverlappedResult( monitor->handle, &monitor->overlapped, &bytes_transferred, TRUE );
}
monitor->running = false;
}
CloseHandle(monitor->handle);
}
monitor->handle = NULL;
}
#endif
void deinit_dirmonitor(struct dirmonitor* monitor) {
#if _WIN32
close_monitor_handle(monitor);
#else
close(monitor->fd);
#endif
free(monitor);
}
int check_dirmonitor(struct dirmonitor* monitor, int (*change_callback)(int, const char*, void*), void* data) {
#if _WIN32
if (!monitor->running) {
if (ReadDirectoryChangesW(monitor->handle, monitor->buffer, sizeof(monitor->buffer), TRUE, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME, NULL, &monitor->overlapped, NULL) == 0)
return GetLastError();
monitor->running = true;
}
DWORD bytes_transferred;
if (!GetOverlappedResult(monitor->handle, &monitor->overlapped, &bytes_transferred, FALSE)) {
int error = GetLastError();
return error == ERROR_IO_PENDING || error == ERROR_IO_INCOMPLETE ? 0 : error;
}
monitor->running = false;
for (FILE_NOTIFY_INFORMATION* info = (FILE_NOTIFY_INFORMATION*)monitor->buffer; (char*)info < monitor->buffer + sizeof(monitor->buffer); info = (FILE_NOTIFY_INFORMATION*)((char*)info) + info->NextEntryOffset) {
change_callback(info->FileNameLength, (char*)info->FileName, data);
if (!info->NextEntryOffset)
break;
}
monitor->running = false;
return 0;
#elif __linux__
char buf[PATH_MAX + sizeof(struct inotify_event)];
while (1) {
ssize_t len = read(monitor->fd, buf, sizeof(buf));
if (len == -1 && errno != EAGAIN)
return errno;
if (len <= 0)
return 0;
for (char *ptr = buf; ptr < buf + len; ptr += sizeof(struct inotify_event) + ((struct inotify_event*)ptr)->len)
change_callback(((const struct inotify_event *) ptr)->wd, NULL, data);
}
#else
struct kevent event;
while (1) {
struct timespec tm = {0};
int nev = kevent(monitor->fd, NULL, 0, &event, 1, &tm);
if (nev == -1)
return errno;
if (nev <= 0)
return 0;
change_callback(event.ident, NULL, data);
}
#endif
}
int add_dirmonitor(struct dirmonitor* monitor, const char* path) {
#if _WIN32
close_monitor_handle(monitor);
monitor->handle = CreateFileA(path, FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL);
if (monitor->handle && monitor->handle != INVALID_HANDLE_VALUE)
return 1;
monitor->handle = NULL;
return -1;
#elif __linux__
return inotify_add_watch(monitor->fd, path, IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO);
#else
int fd = open(path, O_RDONLY);
struct kevent change;
EV_SET(&change, fd, EVFILT_VNODE, EV_ADD | EV_CLEAR, NOTE_DELETE | NOTE_EXTEND | NOTE_WRITE | NOTE_ATTRIB | NOTE_LINK | NOTE_RENAME, 0, (void*)path);
kevent(monitor->fd, &change, 1, NULL, 0, NULL);
return fd;
#endif
}
void remove_dirmonitor(struct dirmonitor* monitor, int fd) {
#if _WIN32
close_monitor_handle(monitor);
#elif __linux__
inotify_rm_watch(monitor->fd, fd);
#else
close(fd);
#endif
}
static int f_check_dir_callback(int watch_id, const char* path, void* L) {
#if _WIN32
char buffer[PATH_MAX*4];
int count = WideCharToMultiByte(CP_UTF8, 0, (WCHAR*)path, watch_id, buffer, PATH_MAX*4 - 1, NULL, NULL);
lua_pushlstring(L, buffer, count);
#else
lua_pushnumber(L, watch_id);
#endif
lua_pcall(L, 1, 1, 0);
int result = lua_toboolean(L, -1);
lua_pop(L, 1);
return !result;
}
static int f_dirmonitor_new(lua_State* L) {
struct dirmonitor** monitor = lua_newuserdata(L, sizeof(struct dirmonitor**));
*monitor = init_dirmonitor();
luaL_setmetatable(L, API_TYPE_DIRMONITOR);
return 1;
}
static int f_dirmonitor_gc(lua_State* L) {
deinit_dirmonitor(*((struct dirmonitor**)luaL_checkudata(L, 1, API_TYPE_DIRMONITOR)));
return 0;
}
static int f_dirmonitor_watch(lua_State *L) {
lua_pushnumber(L, add_dirmonitor(*(struct dirmonitor**)luaL_checkudata(L, 1, API_TYPE_DIRMONITOR), luaL_checkstring(L, 2)));
return 1;
}
static int f_dirmonitor_unwatch(lua_State *L) {
remove_dirmonitor(*(struct dirmonitor**)luaL_checkudata(L, 1, API_TYPE_DIRMONITOR), luaL_checknumber(L, 2));
return 0;
}
static int f_dirmonitor_check(lua_State* L) {
lua_pushnumber(L, check_dirmonitor(*(struct dirmonitor**)luaL_checkudata(L, 1, API_TYPE_DIRMONITOR), f_check_dir_callback, L));
return 1;
}
static const luaL_Reg dirmonitor_lib[] = {
{ "new", f_dirmonitor_new },
{ "__gc", f_dirmonitor_gc },
{ "watch", f_dirmonitor_watch },
{ "unwatch", f_dirmonitor_unwatch },
{ "check", f_dirmonitor_check },
{NULL, NULL}
};
int luaopen_dirmonitor(lua_State* L) {
luaL_newmetatable(L, API_TYPE_DIRMONITOR);
luaL_setfuncs(L, dirmonitor_lib, 0);
lua_pushvalue(L, -1);
lua_setfield(L, -2, "__index");
return 1;
}

View File

@ -6,7 +6,6 @@
#include <errno.h> #include <errno.h>
#include <sys/stat.h> #include <sys/stat.h>
#include "api.h" #include "api.h"
// #include "dirmonitor.h"
#include "rencache.h" #include "rencache.h"
#ifdef _WIN32 #ifdef _WIN32
#include <direct.h> #include <direct.h>
@ -249,26 +248,6 @@ top:
lua_pushnumber(L, e.wheel.y); lua_pushnumber(L, e.wheel.y);
return 2; return 2;
case SDL_USEREVENT:
lua_pushstring(L, "dirchange");
lua_pushnumber(L, e.user.code >> 16);
// switch (e.user.code & 0xffff) {
// case DMON_ACTION_DELETE:
// lua_pushstring(L, "delete");
// break;
// case DMON_ACTION_CREATE:
// lua_pushstring(L, "create");
// break;
// case DMON_ACTION_MODIFY:
// lua_pushstring(L, "modify");
// break;
// default:
// return luaL_error(L, "unknown dmon event action: %d", e.user.code & 0xffff);
// }
// lua_pushstring(L, e.user.data1);
free(e.user.data1);
return 4;
default: default:
goto top; goto top;
} }
@ -794,36 +773,6 @@ static int f_load_native_plugin(lua_State *L) {
return result; return result;
} }
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);
uint32_t dmon_flags = 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);
lua_pushnumber(L, 0);
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 #ifdef _WIN32
#define PATHSEP '\\' #define PATHSEP '\\'
#else #else
@ -909,11 +858,8 @@ static const luaL_Reg lib[] = {
{ "fuzzy_match", f_fuzzy_match }, { "fuzzy_match", f_fuzzy_match },
{ "set_window_opacity", f_set_window_opacity }, { "set_window_opacity", f_set_window_opacity },
{ "load_native_plugin", f_load_native_plugin }, { "load_native_plugin", f_load_native_plugin },
{ "watch_dir", f_watch_dir },
{ "path_compare", f_path_compare }, { "path_compare", f_path_compare },
#if __linux__ #if __linux__
{ "watch_dir_add", f_watch_dir_add },
{ "watch_dir_rm", f_watch_dir_rm },
{ "get_fs_type", f_get_fs_type }, { "get_fs_type", f_get_fs_type },
#endif #endif
{ NULL, NULL } { NULL, NULL }

View File

@ -1,59 +0,0 @@
#include <stdio.h>
#include <string.h>
#include <SDL.h>
#define DMON_IMPL
#include "dmon.h"
#include "dmon_extra.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;
default:
send_sdl_event(watch_id, action, filepath);
}
}

View File

@ -1,15 +0,0 @@
#ifndef DIRMONITOR_H
#define DIRMONITOR_H
#include <stdint.h>
#include "dmon.h"
#include "dmon_extra.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

View File

@ -7,7 +7,7 @@
#ifdef _WIN32 #ifdef _WIN32
#include <windows.h> #include <windows.h>
#elif __linux__ #elif __linux__ || __FreeBSD__
#include <unistd.h> #include <unistd.h>
#include <signal.h> #include <signal.h>
#elif __APPLE__ #elif __APPLE__
@ -16,8 +16,6 @@
#include "platform/amigaos4.h" #include "platform/amigaos4.h"
#endif #endif
#include "dirmonitor.h"
SDL_Window *window; SDL_Window *window;
@ -112,8 +110,6 @@ int main(int argc, char **argv) {
SDL_DisplayMode dm; SDL_DisplayMode dm;
SDL_GetCurrentDisplayMode(0, &dm); SDL_GetCurrentDisplayMode(0, &dm);
// dirmonitor_init();
window = SDL_CreateWindow( window = SDL_CreateWindow(
"", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, dm.w * 0.8, dm.h * 0.8, "", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, dm.w * 0.8, dm.h * 0.8,
SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN); SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN);
@ -201,7 +197,6 @@ init_lua:
lua_close(L); lua_close(L);
ren_free_window_resources(); ren_free_window_resources();
// dirmonitor_deinit();
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }