Repository navigation
Expand file tree
/
Copy pathplatform.m
More file actions
377 lines (340 loc) · 15.3 KB
/
Copy pathplatform.m
File metadata and controls
377 lines (340 loc) · 15.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
#pragma mark - Offscreen render
static NSData *offscreen_render(NSView *view, CGFloat width, CGFloat height) {
NSAppearance *appearance = view.effectiveAppearance;
view.frame = NSMakeRect(0, 0, width, height);
/* Wrap in a borderless offscreen window so drawRect: has a valid window. */
NSWindow *offscreen = [[NSWindow alloc]
initWithContentRect:NSMakeRect(-10000, -10000, width, height)
styleMask:NSWindowStyleMaskBorderless
backing:NSBackingStoreBuffered
defer:NO];
offscreen.releasedWhenClosed = NO;
offscreen.appearance = appearance;
__block NSColor *background;
[appearance performAsCurrentDrawingAppearance:^{
background = [NSColor.windowBackgroundColor
colorUsingColorSpace:NSColorSpace.deviceRGBColorSpace];
}];
offscreen.backgroundColor = background;
offscreen.opaque = YES;
offscreen.contentView.wantsLayer = YES;
offscreen.contentView.layer.backgroundColor = background.CGColor;
[offscreen.contentView addSubview:view];
[offscreen orderBack:nil];
NSView *renderRoot = offscreen.contentView;
NSBitmapImageRep *rep = [renderRoot
bitmapImageRepForCachingDisplayInRect:renderRoot.bounds];
if (!rep) {
[view removeFromSuperview];
[offscreen close];
return nil;
}
CGFloat scale = [NSScreen mainScreen]
? [NSScreen mainScreen].backingScaleFactor : kFallbackBackingScale;
(void)scale;
[renderRoot cacheDisplayInRect:renderRoot.bounds toBitmapImageRep:rep];
NSData *png = [rep representationUsingType:NSBitmapImageFileTypePNG
properties:@{}];
[view removeFromSuperview];
[offscreen close];
return png;
}
#pragma mark - File system watcher
/* FSEvents for a file or a directory tree. Each watch is its own stream with
* its own callback, stopped by `_unwatch(token)` or when its Scope closes;
* the Lua handle in AppKit.watch owns that lifetime. `since` replays events
* recorded after an earlier event ID, so a watcher can learn what changed
* while the app was not running. */
@interface LuaWatcher : NSObject
@property (nonatomic) FSEventStreamRef stream;
@property (nonatomic, strong) LuaReg *reg;
@end
@implementation LuaWatcher
- (void)stop {
if (_stream) {
FSEventStreamStop(_stream);
FSEventStreamInvalidate(_stream);
FSEventStreamRelease(_stream);
_stream = NULL;
}
[_reg dispose];
_reg = nil;
}
- (void)dealloc { [self stop]; }
@end
static NSMutableDictionary<NSNumber *, LuaWatcher *> *gWatchers = nil;
static NSInteger gNextWatcher = 0;
static void watcher_callback(ConstFSEventStreamRef streamRef, void *info, size_t count, void *eventPaths,
const FSEventStreamEventFlags *flags, const FSEventStreamEventId *ids) {
(void)streamRef;
LuaWatcher *watcher = gWatchers[@((NSInteger)(intptr_t)info)];
lua_State *L = lua_reg_live_state(watcher.reg);
if (!L || !lua_reg_push(watcher.reg)) return;
char **paths = eventPaths;
lua_createtable(L, (int)count, 0);
for (size_t i = 0; i < count; i++) {
FSEventStreamEventFlags f = flags[i];
lua_createtable(L, 0, 9);
lua_pushstring(L, paths[i]); lua_setfield(L, -2, "path");
lua_pushnumber(L, (lua_Number)ids[i]); lua_setfield(L, -2, "id");
lua_pushboolean(L, (f & kFSEventStreamEventFlagItemCreated) != 0); lua_setfield(L, -2, "created");
lua_pushboolean(L, (f & kFSEventStreamEventFlagItemRemoved) != 0); lua_setfield(L, -2, "removed");
lua_pushboolean(L, (f & kFSEventStreamEventFlagItemRenamed) != 0); lua_setfield(L, -2, "renamed");
lua_pushboolean(L, (f & (kFSEventStreamEventFlagItemModified | kFSEventStreamEventFlagItemInodeMetaMod)) != 0); lua_setfield(L, -2, "modified");
lua_pushboolean(L, (f & kFSEventStreamEventFlagItemIsDir) != 0); lua_setfield(L, -2, "directory");
lua_pushboolean(L, (f & kFSEventStreamEventFlagMustScanSubDirs) != 0); lua_setfield(L, -2, "rescan");
lua_pushboolean(L, (f & kFSEventStreamEventFlagHistoryDone) != 0); lua_setfield(L, -2, "historyDone");
lua_rawseti(L, -2, (lua_Integer)i + 1);
}
lua_objc_pcall(L, 1, 0, "watch");
}
// _watch(paths, callback, since, latency) -> token. `since` is an event ID
// or nil for events from now on.
static int bridge_watch(lua_State *L) {
NSMutableArray<NSString *> *paths = [NSMutableArray array];
if (lua_istable(L, 1)) {
for (lua_Integer i = 1; ; i++) {
lua_rawgeti(L, 1, i);
if (lua_isnil(L, -1)) { lua_pop(L, 1); break; }
[paths addObject:[NSString stringWithUTF8String:luaL_checkstring(L, -1)]];
lua_pop(L, 1);
}
} else {
[paths addObject:[NSString stringWithUTF8String:luaL_checkstring(L, 1)]];
}
if (paths.count == 0) return luaL_error(L, "watch requires a path");
luaL_checktype(L, 2, LUA_TFUNCTION);
FSEventStreamEventId since = lua_isnumber(L, 3) ? (FSEventStreamEventId)lua_tonumber(L, 3) : kFSEventStreamEventIdSinceNow;
CFTimeInterval latency = luaL_optnumber(L, 4, kFSWatcherLatency);
if (!gWatchers) gWatchers = [NSMutableDictionary dictionary];
NSInteger token = ++gNextWatcher;
FSEventStreamContext ctx = {.version = 0, .info = (void *)(intptr_t)token};
FSEventStreamRef stream = FSEventStreamCreate(NULL, watcher_callback, &ctx, (__bridge CFArrayRef)paths, since, latency,
kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagNoDefer);
if (!stream) return luaL_error(L, "could not watch %s", paths.firstObject.UTF8String);
LuaWatcher *watcher = [LuaWatcher new];
watcher.stream = stream;
watcher.reg = lua_reg_create(L, 2, NO);
gWatchers[@(token)] = watcher;
FSEventStreamSetDispatchQueue(stream, dispatch_get_main_queue());
FSEventStreamStart(stream);
lua_pushinteger(L, token);
return 1;
}
static int bridge_unwatch(lua_State *L) {
NSNumber *token = @(luaL_checkinteger(L, 1));
[(LuaWatcher *)gWatchers[token] stop];
[gWatchers removeObjectForKey:token];
return 0;
}
// The newest event ID on this Mac: store it to ask later what changed since.
static int bridge_latest_event_id(lua_State *L) {
lua_pushnumber(L, (lua_Number)FSEventsGetCurrentEventId());
return 1;
}
/* `_pickFolder(title, options)`: the standard open panel for one folder.
* Options: `directory` it opens at, `message` shown above the browser and
* `prompt` for the default button. A sandboxed app reaches only what the
* person chooses here, so a request for a whole disk opens at the disk and
* says why, as DaisyDisk's does. */
static int bridge_pick_folder(lua_State *L) {
const char *titleC = luaL_optstring(L, 1, "Open Folder");
NSOpenPanel *panel = [NSOpenPanel openPanel];
panel.canChooseFiles = NO;
panel.canChooseDirectories = YES;
panel.allowsMultipleSelection = NO;
panel.canCreateDirectories = YES;
panel.title = [NSString stringWithUTF8String:titleC];
if (lua_istable(L, 2)) {
lua_getfield(L, 2, "directory");
if (lua_isstring(L, -1)) panel.directoryURL = [NSURL fileURLWithPath:@(lua_tostring(L, -1)) isDirectory:YES];
lua_getfield(L, 2, "message");
if (lua_isstring(L, -1)) panel.message = @(lua_tostring(L, -1));
lua_getfield(L, 2, "prompt");
if (lua_isstring(L, -1)) panel.prompt = @(lua_tostring(L, -1));
lua_pop(L, 3);
}
NSInteger response = [panel runModal];
if (response != NSModalResponseOK || panel.URL == nil) {
lua_pushnil(L);
return 1;
}
lua_pushstring(L, panel.URL.path.UTF8String);
return 1;
}
#pragma mark - Navigation gestures
/* Buttons 4 and 5 of a mouse (buttonNumber 3 and 4) and a two-finger swipe
* are how a Mac user goes back and forward in any browsing window. The
* monitor sees only events for its own window and ends with the window. */
static char kNavigationMonitorKey;
@interface LuaNavigationMonitor : NSObject
@property (nonatomic, strong) id monitor;
@property (nonatomic, strong) LuaReg *back;
@property (nonatomic, strong) LuaReg *forward;
@end
@implementation LuaNavigationMonitor
- (void)dealloc {
if (_monitor) [NSEvent removeMonitor:_monitor];
[_back dispose]; [_forward dispose];
}
- (BOOL)navigate:(BOOL)forward {
LuaReg *reg = forward ? self.forward : self.back;
lua_State *L = lua_reg_live_state(reg);
if (!L || !lua_reg_push(reg)) return NO;
lua_objc_pcall(L, 0, 0, forward ? "navigate forward" : "navigate back");
return YES;
}
@end
// _onNavigationGesture(window, back, forward)
static int bridge_on_navigation_gesture(lua_State *L) {
NSWindow *window = lua_objc_check_object(L, 1, [NSWindow class], "window");
LuaNavigationMonitor *navigation = [LuaNavigationMonitor new];
navigation.back = lua_reg_opt(L, 2);
navigation.forward = lua_reg_opt(L, 3);
__weak NSWindow *weakWindow = window;
__weak LuaNavigationMonitor *weakNavigation = navigation;
navigation.monitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskOtherMouseDown | NSEventMaskSwipe
handler:^NSEvent *(NSEvent *event) {
if (event.window != weakWindow) return event;
if (event.type == NSEventTypeOtherMouseDown && (event.buttonNumber == 3 || event.buttonNumber == 4))
return [weakNavigation navigate:event.buttonNumber == 4] ? nil : event;
if (event.type == NSEventTypeSwipe && event.deltaX != 0)
return [weakNavigation navigate:event.deltaX < 0] ? nil : event;
return event;
}];
objc_setAssociatedObject(window, &kNavigationMonitorKey, navigation, OBJC_ASSOCIATION_RETAIN);
return 0;
}
// Test hook: _navigationGesture(window, "back" | "forward")
static int bridge_navigation_gesture(lua_State *L) {
NSWindow *window = lua_objc_check_object(L, 1, [NSWindow class], "window");
LuaNavigationMonitor *navigation = objc_getAssociatedObject(window, &kNavigationMonitorKey);
lua_pushboolean(L, [navigation navigate:strcmp(luaL_checkstring(L, 2), "forward") == 0]);
return 1;
}
#pragma mark - Security-scoped bookmarks
/* A folder the person chose stays reachable across launches, even in a
* sandbox, through a security-scoped bookmark. `_bookmark(path)` returns the
* bookmark as base64 text to store; `_resolveBookmark(text)` returns the path
* (after moves and renames), whether the bookmark is stale and should be
* recreated, and starts access for the rest of the process's life. */
static int bridge_bookmark(lua_State *L) {
NSURL *url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:luaL_checkstring(L, 1)]];
NSError *error = nil;
NSData *data = [url bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
includingResourceValuesForKeys:nil relativeToURL:nil error:&error];
if (!data) { lua_pushnil(L); lua_pushstring(L, error.localizedDescription.UTF8String); return 2; }
lua_pushstring(L, [data base64EncodedStringWithOptions:0].UTF8String);
return 1;
}
static int bridge_resolve_bookmark(lua_State *L) {
NSData *data = [[NSData alloc] initWithBase64EncodedString:[NSString stringWithUTF8String:luaL_checkstring(L, 1)] options:0];
if (!data) { lua_pushnil(L); lua_pushboolean(L, 0); lua_pushstring(L, "Not a bookmark."); return 3; }
BOOL stale = NO;
NSError *error = nil;
NSURL *url = [NSURL URLByResolvingBookmarkData:data options:NSURLBookmarkResolutionWithSecurityScope | NSURLBookmarkResolutionWithoutUI
relativeToURL:nil bookmarkDataIsStale:&stale error:&error];
if (!url) { lua_pushnil(L); lua_pushboolean(L, 0); lua_pushstring(L, error.localizedDescription.UTF8String); return 3; }
[url startAccessingSecurityScopedResource];
lua_pushstring(L, url.path.UTF8String);
lua_pushboolean(L, stale);
return 2;
}
#pragma mark - Home folder
/* `_homeDirectory()`: the person's home folder from the user database.
* Inside the App Sandbox HOME and NSHomeDirectory() name the app's
* container, which is the wrong place to measure or to look for macOS's
* own per-user files. */
static int bridge_home_directory(lua_State *L) {
struct passwd *entry = getpwuid(getuid());
if (!entry || !entry->pw_dir) { lua_pushnil(L); return 1; }
lua_pushstring(L, entry->pw_dir);
return 1;
}
#pragma mark - Application Support
/* `_applicationSupportDirectory(name)`: the app's own folder in Application
* Support, created on first use. NSFileManager resolves the location, so a
* sandboxed build gets its container's folder rather than the home folder's.
* Apple's File System Programming Guide places app-created data the person
* does not manage as documents here; regenerable data belongs in Caches. */
static int bridge_application_support_directory(lua_State *L) {
NSString *name = [NSString stringWithUTF8String:luaL_checkstring(L, 1)];
if (!name.length || [name containsString:@"/"] || [name isEqualToString:@"."] || [name isEqualToString:@".."])
return luaL_error(L, "application support folder name must be one path component");
NSError *error = nil;
NSURL *root = [NSFileManager.defaultManager URLForDirectory:NSApplicationSupportDirectory
inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error];
NSURL *url = [root URLByAppendingPathComponent:name isDirectory:YES];
if (!url || ![NSFileManager.defaultManager createDirectoryAtURL:url withIntermediateDirectories:YES attributes:nil error:&error]) {
lua_pushnil(L);
lua_pushstring(L, error.localizedDescription.UTF8String ?: "Application Support is unavailable.");
return 2;
}
lua_pushstring(L, url.path.UTF8String);
return 1;
}
/* `_pickFile(title, options)`: the standard open panel for one file.
* Options: `types`, a list of Uniform Type Identifiers the panel enables
* ("public.audio" takes every sound file), and `prompt` for the default
* button. */
static int bridge_pick_file(lua_State *L) {
const char *titleC = luaL_optstring(L, 1, "Open File");
NSOpenPanel *panel = [NSOpenPanel openPanel];
panel.canChooseFiles = YES;
panel.canChooseDirectories = NO;
panel.allowsMultipleSelection = NO;
panel.canCreateDirectories = NO;
panel.title = [NSString stringWithUTF8String:titleC];
if (lua_istable(L, 2)) {
lua_getfield(L, 2, "types");
if (lua_istable(L, -1)) {
NSMutableArray<UTType *> *types = [NSMutableArray array];
for (lua_Integer i = 1; lua_rawgeti(L, -1, i) == LUA_TSTRING; i++) {
UTType *type = [UTType typeWithIdentifier:@(lua_tostring(L, -1))];
if (type) [types addObject:type];
lua_pop(L, 1);
}
lua_pop(L, 1);
if (types.count) panel.allowedContentTypes = types;
}
lua_getfield(L, 2, "prompt");
if (lua_isstring(L, -1)) panel.prompt = @(lua_tostring(L, -1));
lua_pop(L, 2);
}
NSInteger response = [panel runModal];
if (response != NSModalResponseOK || panel.URL == nil) {
lua_pushnil(L);
return 1;
}
lua_pushstring(L, panel.URL.path.UTF8String);
return 1;
}
// _saveFile(title, defaultName) -> path or nil: the standard save panel.
static int bridge_save_file(lua_State *L) {
NSSavePanel *panel = [NSSavePanel savePanel];
panel.title = [NSString stringWithUTF8String:luaL_optstring(L, 1, "Save")];
panel.nameFieldStringValue = [NSString stringWithUTF8String:luaL_optstring(L, 2, "Untitled")];
panel.canCreateDirectories = YES;
if ([panel runModal] != NSModalResponseOK || !panel.URL) { lua_pushnil(L); return 1; }
lua_pushstring(L, panel.URL.path.UTF8String);
return 1;
}
#pragma mark - Test support
/* Headless tests schedule real NSTimers but never run NSApp. Pumping the
* current run loop lets a pending timer fire (or prove it was cancelled)
* without showing a window. `"eventTracking"` runs the mode AppKit uses
* while a slider or menu tracks the mouse. */
static int bridge_runloop_tick(lua_State *L) {
double seconds = luaL_optnumber(L, 1, 0.05);
const char *mode = luaL_optstring(L, 2, "default");
NSDate *limit = [NSDate dateWithTimeIntervalSinceNow:seconds];
if (strcmp(mode, "eventTracking") == 0) {
/* NSApplication registers event tracking as a common mode. */
[NSApplication sharedApplication];
while ([limit timeIntervalSinceNow] > 0
&& [NSRunLoop.currentRunLoop runMode:NSEventTrackingRunLoopMode beforeDate:limit]) {}
} else {
[NSRunLoop.currentRunLoop runUntilDate:limit];
}
return 0;
}