hammerspoon/extensions/pathwatcher/libpathwatcher.m

254 lines
10 KiB
Objective-C

#import <Cocoa/Cocoa.h>
#import <LuaSkin/LuaSkin.h>
// Common Code
#define USERDATA_TAG "hs.pathwatcher"
static LSRefTable refTable;
// Not so common code
typedef struct _watcher_path_t {
int closureref;
FSEventStreamRef stream;
bool started;
LSGCCanary lsCanary;
} watcher_path_t;
#if MAC_OS_X_VERSION_MAX_ALLOWED < 1090
static const FSEventStreamEventFlags kFSEventStreamEventFlagOwnEvent = 0x00080000;
#endif
#if MAC_OS_X_VERSION_MAX_ALLOWED < 101000
static const FSEventStreamEventFlags kFSEventStreamEventFlagItemIsHardlink = 0x00100000;
static const FSEventStreamEventFlags kFSEventStreamEventFlagItemIsLastHardlink = 0x00200000;
#endif
static void pusheventflagstable(lua_State* L, FSEventStreamEventFlags flags) {
lua_newtable(L);
if ((flags & kFSEventStreamEventFlagMustScanSubDirs) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "mustScanSubDirs"); }
if ((flags & kFSEventStreamEventFlagUserDropped) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "userDropped"); }
if ((flags & kFSEventStreamEventFlagKernelDropped) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "kernelDropped"); }
if ((flags & kFSEventStreamEventFlagEventIdsWrapped) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "eventIdsWrapped"); }
if ((flags & kFSEventStreamEventFlagHistoryDone) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "historyDone"); }
if ((flags & kFSEventStreamEventFlagRootChanged) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "rootChanged"); }
if ((flags & kFSEventStreamEventFlagMount) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "mount"); }
if ((flags & kFSEventStreamEventFlagUnmount) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "unmount"); }
if ((flags & kFSEventStreamEventFlagOwnEvent) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "ownEvent"); }
if ((flags & kFSEventStreamEventFlagItemCreated) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "itemCreated"); }
if ((flags & kFSEventStreamEventFlagItemRemoved) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "itemRemoved"); }
if ((flags & kFSEventStreamEventFlagItemInodeMetaMod) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "itemInodeMetaMod"); }
if ((flags & kFSEventStreamEventFlagItemRenamed) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "itemRenamed"); }
if ((flags & kFSEventStreamEventFlagItemModified) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "itemModified"); }
if ((flags & kFSEventStreamEventFlagItemFinderInfoMod) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "itemFinderInfoMod"); }
if ((flags & kFSEventStreamEventFlagItemChangeOwner) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "itemChangeOwner"); }
if ((flags & kFSEventStreamEventFlagItemXattrMod) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "itemXattrMod"); }
if ((flags & kFSEventStreamEventFlagItemIsFile) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "itemIsFile"); }
if ((flags & kFSEventStreamEventFlagItemIsDir) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "itemIsDir"); }
if ((flags & kFSEventStreamEventFlagItemIsSymlink) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "itemIsSymlink"); }
if ((flags & kFSEventStreamEventFlagItemIsHardlink) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "itemIsHardlink"); }
if ((flags & kFSEventStreamEventFlagItemIsLastHardlink) != 0) { lua_pushboolean(L, YES); lua_setfield(L, -2, "itemIsLastHardlink"); }
}
void event_callback(ConstFSEventStreamRef __unused streamRef, void *clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId __unused eventIds[]) {
watcher_path_t* pw = clientCallBackInfo;
LuaSkin *skin = [LuaSkin sharedWithState:NULL];
lua_State *L = skin.L;
if (![skin checkGCCanary:pw->lsCanary]) {
return;
}
_lua_stackguard_entry(skin.L);
const char** changedFiles = eventPaths;
[skin pushLuaRef:refTable ref:pw->closureref];
lua_newtable(L);
for(size_t i = 0 ; i < numEvents; i++) {
lua_pushstring(L, changedFiles[i]);
lua_rawseti(L, -2, i + 1);
}
lua_newtable(L);
for(size_t i = 0 ; i < numEvents; i++) {
pusheventflagstable(L, eventFlags[i]);
lua_rawseti(L, -2, i + 1);
}
[skin protectedCallAndError:@"hs.pathwatcher callback" nargs:2 nresults:0];
_lua_stackguard_exit(skin.L);
}
/// hs.pathwatcher.new(path, fn) -> watcher
/// Constructor
/// Creates a new path watcher object
///
/// Parameters:
/// * path - A string containing the path to be watched
/// * fn - A function to be called when changes are detected. It should accept two arguments:
/// * `paths`: a table containing a list of file paths that have changed
/// * `flagTables`: a table containing a list of tables denoting how each corresponding file in `paths` has changed, each containing boolean values indicating which types of events occurred; The possible keys are:
/// * mustScanSubDirs
/// * userDropped
/// * kernelDropped
/// * eventIdsWrapped
/// * historyDone
/// * rootChanged
/// * mount
/// * unmount
/// * itemCreated
/// * itemRemoved
/// * itemInodeMetaMod
/// * itemRenamed
/// * itemModified
/// * itemFinderInfoMod
/// * itemChangeOwner
/// * itemXattrMod
/// * itemIsFile
/// * itemIsDir
/// * itemIsSymlink
/// * ownEvent (OS X 10.9+)
/// * itemIsHardlink (OS X 10.10+)
/// * itemIsLastHardlink (OS X 10.10+)
///
/// Returns:
/// * An `hs.pathwatcher` object
///
/// Notes:
/// * For more information about the event flags, see [the official documentation](https://developer.apple.com/reference/coreservices/1455361-fseventstreameventflags/)
static int watcher_path_new(lua_State* L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
[skin checkArgs:LS_TSTRING, LS_TFUNCTION, LS_TBREAK];
NSString* path = [NSString stringWithUTF8String: lua_tostring(L, 1)];
watcher_path_t* watcher_path = lua_newuserdata(L, sizeof(watcher_path_t));
watcher_path->started = NO;
watcher_path->lsCanary = [skin createGCCanary];
luaL_getmetatable(L, USERDATA_TAG);
lua_setmetatable(L, -2);
lua_pushvalue(L, 2);
watcher_path->closureref = [skin luaRef:refTable];
FSEventStreamContext context;
context.info = watcher_path;
context.version = 0;
context.retain = NULL;
context.release = NULL;
context.copyDescription = NULL;
watcher_path->stream = FSEventStreamCreate(NULL,
event_callback,
&context,
(__bridge CFArrayRef)@[[[path stringByStandardizingPath] stringByResolvingSymlinksInPath]],
kFSEventStreamEventIdSinceNow,
0.4,
kFSEventStreamCreateFlagWatchRoot | kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagFileEvents);
return 1;
}
/// hs.pathwatcher:start()
/// Method
/// Starts a path watcher
///
/// Parameters:
/// * None
///
/// Returns:
/// * The `hs.pathwatcher` object
static int watcher_path_start(lua_State* L) {
watcher_path_t* watcher_path = luaL_checkudata(L, 1, USERDATA_TAG);
lua_settop(L, 1);
if (watcher_path->started) return 1;
watcher_path->started = YES;
FSEventStreamScheduleWithRunLoop(watcher_path->stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
FSEventStreamStart(watcher_path->stream);
return 1;
}
/// hs.pathwatcher:stop()
/// Method
/// Stops a path watcher
///
/// Parameters:
/// * None
///
/// Returns:
/// * None
static int watcher_path_stop(lua_State* L) {
watcher_path_t* watcher_path = luaL_checkudata(L, 1, USERDATA_TAG);
lua_settop(L, 1);
if (!watcher_path->started) return 1;
watcher_path->started = NO;
FSEventStreamStop(watcher_path->stream);
FSEventStreamUnscheduleFromRunLoop(watcher_path->stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
return 1;
}
static int watcher_path_gc(lua_State* L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
watcher_path_t* watcher_path = luaL_checkudata(L, 1, USERDATA_TAG);
lua_pushcfunction(L, watcher_path_stop) ; lua_pushvalue(L,1); lua_call(L, 1, 1);
FSEventStreamInvalidate(watcher_path->stream);
FSEventStreamRelease(watcher_path->stream);
watcher_path->closureref = [skin luaUnref:refTable ref:watcher_path->closureref];
[skin destroyGCCanary:&(watcher_path->lsCanary)];
return 0;
}
static int meta_gc(lua_State* __unused L) {
return 0;
}
static int userdata_tostring(lua_State* L) {
watcher_path_t* watcher_path = luaL_checkudata(L, 1, USERDATA_TAG);
NSArray *thePaths = (__bridge_transfer NSArray *) FSEventStreamCopyPathsBeingWatched (watcher_path->stream);
NSString *thePath = [thePaths objectAtIndex:0] ;
if (!thePath) thePath = @"(unknown path)" ;
lua_pushstring(L, [[NSString stringWithFormat:@"%s: %@ (%p)", USERDATA_TAG, thePath, lua_topointer(L, 1)] UTF8String]) ;
return 1 ;
}
// Metatable for created objects when _new invoked
static const luaL_Reg path_metalib[] = {
{"start", watcher_path_start},
{"stop", watcher_path_stop},
{"__gc", watcher_path_gc},
{"__tostring", userdata_tostring},
{NULL, NULL}
};
// Functions for returned object when module loads
static const luaL_Reg pathLib[] = {
{"new", watcher_path_new},
{NULL, NULL}
};
// Metatable for returned object when module loads
static const luaL_Reg meta_gcLib[] = {
{"__gc", meta_gc},
{NULL, NULL}
};
int luaopen_hs_libpathwatcher(lua_State* L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
refTable = [skin registerLibraryWithObject:USERDATA_TAG functions:pathLib metaFunctions:meta_gcLib objectFunctions:path_metalib];
return 1;
}