334 lines
13 KiB
Objective-C
334 lines
13 KiB
Objective-C
@import Cocoa ;
|
|
@import LuaSkin ;
|
|
|
|
// establish a unique context for identifying our observers
|
|
//static const char * const USERDATA_TAG = "hs.settings" ;
|
|
static void *myKVOContext = &myKVOContext ; // See http://nshipster.com/key-value-observing/
|
|
static LSRefTable refTable = LUA_NOREF ;
|
|
|
|
@interface HSUserDefaultKVOWatcher : NSObject ;
|
|
@property NSMutableDictionary *watchedKeys ;
|
|
@end
|
|
|
|
@implementation HSUserDefaultKVOWatcher
|
|
- (instancetype)init {
|
|
self = [super init] ;
|
|
if (self) {
|
|
_watchedKeys = [[NSMutableDictionary alloc] init] ;
|
|
}
|
|
return self ;
|
|
}
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
|
|
if (context != myKVOContext) {
|
|
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
|
return;
|
|
}
|
|
|
|
// [LuaSkin logWarn:[NSString stringWithFormat:@"in observeValueForKeyPath for %@ with %@", keyPath, change]] ;
|
|
if (context == myKVOContext && _watchedKeys && _watchedKeys[keyPath]) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
NSMutableDictionary *fnCallbacks = self->_watchedKeys[keyPath] ;
|
|
// [LuaSkin logWarn:[NSString stringWithFormat:@"in callback for %@ with %@", keyPath, fnCallbacks]] ;
|
|
LuaSkin *skin = [LuaSkin sharedWithState:NULL] ;
|
|
_lua_stackguard_entry(skin.L);
|
|
[fnCallbacks enumerateKeysAndObjectsUsingBlock:^(NSString *watcherID, NSNumber *refN, __unused BOOL *stop) {
|
|
[skin pushLuaRef:refTable ref:refN.intValue] ;
|
|
[skin pushNSObject:keyPath] ;
|
|
[skin protectedCallAndError:[NSString stringWithFormat:@"hs.settings:watcher %@ callback", watcherID] nargs:1 nresults:0];
|
|
}] ;
|
|
_lua_stackguard_exit(skin.L);
|
|
});
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
static HSUserDefaultKVOWatcher *watcherManager ;
|
|
|
|
/// hs.settings.set(key[, val])
|
|
/// Function
|
|
/// Saves a setting with common datatypes
|
|
///
|
|
/// Parameters:
|
|
/// * key - A string containing the name of the setting
|
|
/// * val - An optional value for the setting. Valid datatypes are:
|
|
/// * string
|
|
/// * number
|
|
/// * boolean
|
|
/// * nil
|
|
/// * table (which may contain any of the same valid datatypes)
|
|
///
|
|
/// Returns:
|
|
/// * None
|
|
///
|
|
/// Notes:
|
|
/// * If no val parameter is provided, it is assumed to be nil
|
|
/// * This function cannot set dates or raw data types, see `hs.settings.setDate()` and `hs.settings.setData()`
|
|
/// * Assigning a nil value is equivalent to clearing the value with `hs.settings.clear`
|
|
static int target_set(lua_State* L) {
|
|
LuaSkin * skin = [LuaSkin sharedWithState:L] ;
|
|
[skin checkArgs:LS_TSTRING, LS_TANY | LS_TOPTIONAL, LS_TBREAK] ;
|
|
NSString* key = [NSString stringWithUTF8String: luaL_checkstring(L, 1)];
|
|
if (!key) return luaL_error(L, "key must be a valid UTF8 string") ;
|
|
|
|
// Allow for missing second argument for backwards compatibility with pre-LuaSkin behavior
|
|
id val = nil ;
|
|
if (lua_gettop(L) == 2) {
|
|
val = [skin toNSObjectAtIndex:2 withOptions:LS_NSPreserveLuaStringExactly | LS_NSRawTables] ;
|
|
}
|
|
|
|
@try {
|
|
[[NSUserDefaults standardUserDefaults] setObject:val forKey:key];
|
|
}
|
|
@catch(NSException *theException) {
|
|
[NSUserDefaults resetStandardUserDefaults] ;
|
|
return luaL_error(L, [[NSString stringWithFormat:@"%@: %@", theException.name, theException.reason] UTF8String]);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/// hs.settings.setData(key, val)
|
|
/// Function
|
|
/// Saves a setting with raw binary data
|
|
///
|
|
/// Parameters:
|
|
/// * key - A string containing the name of the setting
|
|
/// * val - Some raw binary data
|
|
///
|
|
/// Returns:
|
|
/// * None
|
|
static int target_setData(lua_State* L) {
|
|
[[LuaSkin sharedWithState:L] checkArgs:LS_TSTRING, LS_TSTRING, LS_TBREAK] ;
|
|
NSString* key = [NSString stringWithUTF8String: luaL_checkstring(L, 1)];
|
|
if (!key) return luaL_error(L, "key must be a valid UTF8 string") ;
|
|
if (lua_type(L,2) == LUA_TSTRING) {
|
|
const char* data = lua_tostring(L,2) ;
|
|
NSUInteger sz = lua_rawlen(L, 2) ;
|
|
NSData* myData = [[NSData alloc] initWithBytes:data length:sz] ;
|
|
[[NSUserDefaults standardUserDefaults] setObject:myData forKey:key];
|
|
} else {
|
|
luaL_error(L, "second argument not (binary data encapsulated as) a string") ;
|
|
}
|
|
|
|
return 0 ;
|
|
}
|
|
|
|
static NSDate* date_from_string(NSString* dateString) {
|
|
// rfc3339 (Internet Date/Time) formated date. More or less.
|
|
NSDateFormatter *rfc3339DateFormatter = [[NSDateFormatter alloc] init];
|
|
NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
|
|
[rfc3339DateFormatter setLocale:enUSPOSIXLocale];
|
|
[rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"];
|
|
[rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
|
|
|
|
NSDate *date = [rfc3339DateFormatter dateFromString:dateString];
|
|
return date;
|
|
}
|
|
|
|
/// hs.settings.setDate(key, val)
|
|
/// Function
|
|
/// Saves a setting with a date
|
|
///
|
|
/// Parameters:
|
|
/// * key - A string containing the name of the setting
|
|
/// * val - A number representing seconds since `1970-01-01 00:00:00 +0000` (e.g. `os.time()`), or a string containing a date in RFC3339 format (`YYYY-MM-DD[T]HH:MM:SS[Z]`)
|
|
///
|
|
/// Returns:
|
|
/// * None
|
|
///
|
|
/// Notes:
|
|
/// * See `hs.settings.dateFormat` for a convenient representation of the RFC3339 format, to use with other time/date related functions
|
|
static int target_setDate(lua_State* L) {
|
|
[[LuaSkin sharedWithState:L] checkArgs:LS_TSTRING, LS_TSTRING | LS_TNUMBER, LS_TBREAK] ;
|
|
|
|
NSString* key = [NSString stringWithUTF8String: luaL_checkstring(L, 1)];
|
|
if (!key) return luaL_error(L, "key must be a valid UTF8 string") ;
|
|
NSDate* myDate = lua_isnumber(L, 2) ? [[NSDate alloc] initWithTimeIntervalSince1970:(NSTimeInterval) lua_tonumber(L,2)] :
|
|
lua_isstring(L, 2) ? date_from_string([NSString stringWithUTF8String:lua_tostring(L, 2)]) : nil ;
|
|
if (myDate) {
|
|
[[NSUserDefaults standardUserDefaults] setObject:myDate forKey:key];
|
|
} else {
|
|
luaL_error(L, "Not a date type -- Number: # of seconds since 1970-01-01 00:00:00Z or String: in the format of 'YYYY-MM-DD[T]HH:MM:SS[Z]' (rfc3339)") ;
|
|
}
|
|
return 0 ;
|
|
}
|
|
|
|
/// hs.settings.get(key) -> string or boolean or number or nil or table or binary data
|
|
/// Function
|
|
/// Loads a setting
|
|
///
|
|
/// Parameters:
|
|
/// * key - A string containing the name of the setting
|
|
///
|
|
/// Returns:
|
|
/// * The value of the setting
|
|
///
|
|
/// Notes:
|
|
/// * This function can load all of the datatypes supported by `hs.settings.set()`, `hs.settings.setData()` and `hs.settings.setDate()`
|
|
static int target_get(lua_State* L) {
|
|
LuaSkin *skin = [LuaSkin sharedWithState:L] ;
|
|
[skin checkArgs:LS_TSTRING, LS_TBREAK] ;
|
|
NSString* key = [NSString stringWithUTF8String: luaL_checkstring(L, 1)];
|
|
if (!key) return luaL_error(L, "key must be a valid UTF8 string") ;
|
|
id val = [[NSUserDefaults standardUserDefaults] objectForKey:key];
|
|
[skin pushNSObject:val] ;
|
|
return 1;
|
|
}
|
|
|
|
/// hs.settings.clear(key) -> bool
|
|
/// Function
|
|
/// Deletes a setting
|
|
///
|
|
/// Parameters:
|
|
/// * key - A string containing the name of a setting
|
|
///
|
|
/// Returns:
|
|
/// * A boolean, true if the setting was deleted, otherwise false
|
|
static int target_clear(lua_State* L) {
|
|
[[LuaSkin sharedWithState:L] checkArgs:LS_TSTRING, LS_TBREAK] ;
|
|
NSString* key = [NSString stringWithUTF8String: luaL_checkstring(L, 1)];
|
|
if (!key) return luaL_error(L, "key must be a valid UTF8 string") ;
|
|
if ([[NSUserDefaults standardUserDefaults] objectForKey:key] && ![[NSUserDefaults standardUserDefaults] objectIsForcedForKey:key]) {
|
|
[[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
|
|
lua_pushboolean(L, YES) ;
|
|
} else
|
|
lua_pushboolean(L, NO) ;
|
|
return 1;
|
|
}
|
|
|
|
/// hs.settings.getKeys() -> table
|
|
/// Function
|
|
/// Gets all of the previously stored setting names
|
|
///
|
|
/// Parameters:
|
|
/// * None
|
|
///
|
|
/// Returns:
|
|
/// * A table containing all of the settings keys in Hammerspoon's settings
|
|
///
|
|
/// Notes:
|
|
/// * Use `ipairs(hs.settings.getKeys())` to iterate over all available settings
|
|
/// * Use `hs.settings.getKeys()["someKey"]` to test for the existance of a particular key
|
|
static int target_getKeys(lua_State* L) {
|
|
LuaSkin * skin = [LuaSkin sharedWithState:L] ;
|
|
[skin checkArgs:LS_TBREAK] ;
|
|
NSString *mainID = [[NSBundle mainBundle] bundleIdentifier] ;
|
|
NSArray *keys = [[[NSUserDefaults standardUserDefaults] persistentDomainForName:mainID] allKeys];
|
|
lua_newtable(L);
|
|
for (unsigned long i = 0; i < keys.count; i++) {
|
|
lua_pushinteger(L, (lua_Integer)i+1) ;
|
|
[skin pushNSObject:[keys objectAtIndex:i]] ;
|
|
lua_settable(L, -3);
|
|
[skin pushNSObject:[keys objectAtIndex:i]] ;
|
|
lua_pushboolean(L, YES) ;
|
|
lua_settable(L, -3);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/// hs.settings.watchKey(identifier, key, [fn]) -> identifier | current value
|
|
/// Function
|
|
/// Get or set a watcher to invoke a callback when the specified settings key changes
|
|
///
|
|
/// Parameters:
|
|
/// * identifier - a required string used as an identifier for this callback
|
|
/// * key - the settings key to watch for changes to
|
|
/// * fn - the callback function to be invoked when the specified key changes. If this is an explicit nil, removes the existing callback.
|
|
///
|
|
/// Returns:
|
|
/// * if a callback is set or removed, returns the identifier; otherwise returns the current callback function or nil if no callback function is currently defined.
|
|
///
|
|
/// Notes:
|
|
/// * the identifier is required so that multiple callbacks for the same key can be registered by separate modules; it's value doesn't affect what is being watched but does need to be unique between multiple watchers of the same key.
|
|
/// * Does not work with keys that include a period (.) in the key name because KVO uses dot notation to specify a sequence of properties. If you know of a way to escape periods so that they are watchable as NSUSerDefault key names, please file an issue and share!
|
|
static int target_watchKey(lua_State *L) {
|
|
LuaSkin *skin = [LuaSkin sharedWithState:L] ;
|
|
[skin checkArgs:LS_TSTRING, LS_TSTRING, LS_TFUNCTION | LS_TNIL | LS_TOPTIONAL, LS_TBREAK] ;
|
|
NSString *watcherID = [skin toNSObjectAtIndex:1] ;
|
|
NSString *keyPath = [skin toNSObjectAtIndex:2] ;
|
|
|
|
if (!watcherManager.watchedKeys[keyPath]) {
|
|
watcherManager.watchedKeys[keyPath] = [[NSMutableDictionary alloc] init] ;
|
|
[[NSUserDefaults standardUserDefaults] addObserver:watcherManager forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:myKVOContext] ;
|
|
}
|
|
NSMutableDictionary *keyWatchers = watcherManager.watchedKeys[keyPath] ;
|
|
NSNumber *refN = keyWatchers[watcherID] ;
|
|
|
|
if (lua_gettop(L) == 2) {
|
|
if (refN) {
|
|
[skin pushLuaRef:refTable ref:refN.intValue] ;
|
|
} else {
|
|
lua_pushnil(L) ;
|
|
}
|
|
} else {
|
|
if (refN) [skin luaUnref:refTable ref:refN.intValue] ;
|
|
keyWatchers[watcherID] = nil ;
|
|
if (lua_type(L, 3) != LUA_TNIL) {
|
|
lua_pushvalue(L, 3) ;
|
|
keyWatchers[watcherID] = @([skin luaRef:refTable]) ;
|
|
}
|
|
lua_pushvalue(L, 1) ;
|
|
}
|
|
return 1 ;
|
|
}
|
|
|
|
// for debugging, should probably be removed at some point
|
|
static int output_watchers(lua_State *L) {
|
|
[[LuaSkin sharedWithState:L] pushNSObject:watcherManager.watchedKeys] ;
|
|
return 1 ;
|
|
}
|
|
|
|
static int meta_gc(lua_State* L) {
|
|
LuaSkin *skin = [LuaSkin sharedWithState:L] ;
|
|
[watcherManager.watchedKeys enumerateKeysAndObjectsUsingBlock:^(NSString *keyPath, NSMutableDictionary *watchers, __unused BOOL *outterStop) {
|
|
[[NSUserDefaults standardUserDefaults] removeObserver:watcherManager forKeyPath:keyPath context:myKVOContext] ;
|
|
[watchers enumerateKeysAndObjectsUsingBlock:^(__unused NSString *watcherID, NSNumber *refN, __unused BOOL *innerStop) {
|
|
[skin luaUnref:refTable ref:refN.intValue] ;
|
|
}] ;
|
|
}] ;
|
|
watcherManager.watchedKeys = nil ;
|
|
watcherManager = nil ;
|
|
return 0 ;
|
|
}
|
|
|
|
// Functions for returned object when module loads
|
|
static const luaL_Reg settingslib[] = {
|
|
{"set", target_set},
|
|
{"setData", target_setData},
|
|
{"setDate", target_setDate},
|
|
{"get", target_get},
|
|
{"clear", target_clear},
|
|
{"getKeys", target_getKeys},
|
|
{"watchKey", target_watchKey},
|
|
{"_watchers", output_watchers},
|
|
{NULL, NULL}
|
|
};
|
|
|
|
// Metatable for module, if needed
|
|
static const luaL_Reg module_metaLib[] = {
|
|
{"__gc", meta_gc},
|
|
{NULL, NULL}
|
|
};
|
|
|
|
int luaopen_hs_libsettings(lua_State* L) {
|
|
LuaSkin *skin = [LuaSkin sharedWithState:L];
|
|
refTable = [skin registerLibrary:"hs.settings" functions:settingslib metaFunctions:module_metaLib];
|
|
|
|
watcherManager = [[HSUserDefaultKVOWatcher alloc] init] ;
|
|
|
|
/// hs.settings.dateFormat
|
|
/// Constant
|
|
/// A string representing the expected format of date and time when presenting the date and time as a string to `hs.setDate()`. e.g. `os.date(hs.settings.dateFormat)`
|
|
lua_pushstring(skin.L, "!%Y-%m-%dT%H:%M:%SZ") ;
|
|
lua_setfield(skin.L, -2, "dateFormat") ;
|
|
|
|
/// hs.settings.bundleID
|
|
/// Constant
|
|
/// A string representing the ID of the bundle Hammerspoon's settings are stored in . You can use this with the command line tool `defaults` or other tools which allow access to the `User Defaults` of applications, to access these outside of Hammerspoon
|
|
lua_pushstring(skin.L, [[[NSBundle mainBundle] bundleIdentifier] UTF8String]) ;
|
|
lua_setfield(skin.L, -2, "bundleID") ;
|
|
|
|
return 1;
|
|
}
|