Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions examples/watch_directory.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
local fs = require("@std/fs")
local path = require("@std/path")
local system = require("@std/system")
local task = require("@lute/task")

-- Setup, get a temporary directory to watch
local tmpdir = system.tmpdir()

local watchedFileName = "watched.txt"
local watched = path.join(tmpdir, watchedFileName)
if fs.exists(watched) then
fs.remove(watched)
end

local watcher = fs.watch(tmpdir)

-- Trigger a file change event
fs.writestringtofile(watched, "x")

-- Poll the iterator with a timeout of 2 seconds
local event
local start = os.clock()

repeat
event = watcher:next()
if not event then
task.wait(0.01)
end
until event or (os.clock() - start > 2)

if event then
print("Event detected:", event.change and "change" or "rename")
else
print("No event detected within the timeout period.")
end

-- Cleanup, close the watcher and remove the watched file
watcher:close()
if fs.exists(watched) then
fs.remove(watched)
end
25 changes: 4 additions & 21 deletions lute/fs/src/fs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -636,27 +636,10 @@ int fs_watch(lua_State* L)
// events
lua_createtable(L, 0, 2);

if ((events & UV_RENAME) == UV_RENAME)
{
lua_pushboolean(L, true);
lua_setfield(L, -2, "rename");
}
else
{
lua_pushboolean(L, false);
lua_setfield(L, -2, "rename");
}

if ((events & UV_CHANGE) == UV_CHANGE)
{
lua_pushboolean(L, true);
lua_setfield(L, -2, "change");
}
else
{
lua_pushboolean(L, false);
lua_setfield(L, -2, "change");
}
lua_pushboolean(L, (events & UV_RENAME) != 0);
lua_setfield(L, -2, "rename");
lua_pushboolean(L, (events & UV_CHANGE) != 0);
lua_setfield(L, -2, "change");

return 2;
}
Expand Down
32 changes: 29 additions & 3 deletions lute/std/libs/fs.luau
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export type walkoptions = {
recursive: boolean?,
}

type watcher = {
next: (watcher) -> watchevent?,
close: (self: watcher) -> (),
}

function fslib.open(path: pathlike, mode: handlemode?): file
return fs.open(pathlib.format(path), mode)
end
Expand Down Expand Up @@ -68,8 +73,29 @@ function fslib.symboliclink(src: pathlike, dest: pathlike): ()
return fs.symlink(pathlib.format(src), pathlib.format(dest))
end

function fslib.watch(path: pathlike, callback: (filename: pathlike, event: watchevent) -> ()): watchhandle
return fs.watch(pathlib.format(path), callback)
--- Iterator function that yields filename and event pairs when changes occur in the watched path.
--- Also provides a `close` method to stop watching.
--- Note: for loops do not support yielding generalized iterators, so we cannot use fs.watch as `for _ in fs.watch(...) do` directly. A while loop can be used instead. See example/watch_directory.luau for usage.
function fslib.watch(path: pathlike): watcher
local queue = {}
local handle = fs.watch(pathlib.format(path), function(filename: string, event: watchevent)
table.insert(queue, { filename = pathlib.parse(filename), event = event })
end)

return {
next = function(self: watcher): watchevent?
if #queue == 0 then
return nil
end
local item = table.remove(queue, 1)
return item.event
end,
close = function(self: watcher): ()
if handle then
handle:close()
end
end,
}
end

function fslib.exists(path: pathlike): boolean
Expand Down Expand Up @@ -139,7 +165,7 @@ function fslib.removedirectory(path: pathlike, options: removedirectoryoptions?)
end
end

-- Note: for loops do not support yielding generalized iterators, so we cannot use fs.walk as `for path in fs.walk(...) do` directly. A while loop can be used instead.
--- Note: for loops do not support yielding generalized iterators, so we cannot use fs.walk as `for path in fs.walk(...) do` directly. A while loop can be used instead. See example/walk_directory.luau for usage.
function fslib.walk(path: pathlike, options: walkoptions?): () -> path?
local queue = { path }

Expand Down
2 changes: 0 additions & 2 deletions tests/lute/task.test.luau
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ test.suite("LuteTaskSuite", function(suite)
task.wait(0.5)
local endTime = os.clock()

print("Elapsed time: ", endTime - startTime)

-- task.wait(0.5) actually waits ~0.48 seconds :)
assert.eq(math.ceil(endTime - startTime) >= 0.5, true)
end)
Expand Down
30 changes: 17 additions & 13 deletions tests/std/fs.test.luau
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ local fs = require("@std/fs")
local path = require("@std/path")
local system = require("@std/system")
local test = require("@std/test")
local task = require("@lute/task")

local tmpdir = system.tmpdir()

Expand Down Expand Up @@ -89,28 +90,31 @@ test.suite("FsSuite", function(suite)
fs.remove(src)
end)

suite:case("watch_callback_on_change", function(assert)
local watched = path.join(tmpdir, "watched.txt")
suite:case("watch_iterator_on_change", function(assert)
local watchedFileName = "watched.txt"
local watched = path.join(tmpdir, watchedFileName)
if fs.exists(watched) then
fs.remove(watched)
end

local triggered = false
local handle = fs.watch(tmpdir, function(filename, event)
if tostring(filename):find("watched.txt") then
triggered = true
end
end)
local watcher = fs.watch(tmpdir)

fs.writestringtofile(watched, "x")

local start = os.time()
while not triggered and os.time() - start < 2 do
end
local start = os.clock()
local event

repeat
event = watcher:next()
if not event then
task.wait(0.01)
end
until event or (os.clock() - start > 2)

assert.eq(type(triggered) == "boolean", true)
assert.neq(event, nil)
assert.eq(event.change or event.rename, true)

handle:close()
watcher:close()
if fs.exists(watched) then
fs.remove(watched)
end
Expand Down