Revision: 13796
http://sourceforge.net/p/skim-app/code/13796
Author: hofman
Date: 2023-11-24 15:21:52 +0000 (Fri, 24 Nov 2023)
Log Message:
-----------
Add optional use of XPC connection to get skim notes for test app
Modified Paths:
--------------
trunk/SkimNotes/SKNDocument.h
trunk/SkimNotes/SKNDocument.m
trunk/SkimNotes/SKNSkimReader.h
trunk/SkimNotes/SKNSkimReader.m
trunk/SkimNotes/SkimNotes.xcodeproj/project.pbxproj
Added Paths:
-----------
trunk/SkimNotes/SKNXPCSkimReader.h
trunk/SkimNotes/SKNXPCSkimReader.m
Modified: trunk/SkimNotes/SKNDocument.h
===================================================================
--- trunk/SkimNotes/SKNDocument.h 2023-11-24 14:49:15 UTC (rev 13795)
+++ trunk/SkimNotes/SKNDocument.h 2023-11-24 15:21:52 UTC (rev 13796)
@@ -39,10 +39,11 @@
#import <Cocoa/Cocoa.h>
-// Uncomment one of these three #defines
+// Uncomment one of these four #defines
#define FrameworkSample
//#define AgentSample
+//#define XPCAgentSample
//#define ToolSample
@interface SKNDocument : NSDocument {
Modified: trunk/SkimNotes/SKNDocument.m
===================================================================
--- trunk/SkimNotes/SKNDocument.m 2023-11-24 14:49:15 UTC (rev 13795)
+++ trunk/SkimNotes/SKNDocument.m 2023-11-24 15:21:52 UTC (rev 13796)
@@ -39,6 +39,7 @@
#import "SKNDocument.h"
#import <SkimNotesBase/SkimNotesBase.h>
#import "SKNSkimReader.h"
+#import "SKNXPCSkimReader.h"
#define SKNPDFDocumentType @"com.adobe.pdf"
#define SKNPDFBundleDocumentType @"net.sourceforge.skim-app.pdfd"
@@ -53,11 +54,15 @@
+ (void)initialize {
[NSValueTransformer setValueTransformer:[[[SKNPlusOneTransformer alloc]
init] autorelease] forName:@"SKNPlusOne"];
}
-
+
++ (BOOL)autosavesInPlace {
+ return NO;
+}
+
- (id)init {
self = [super init];
if (self) {
- notes = [[NSArray alloc] init];
+ notes = [[NSArray alloc] init];
}
return self;
}
@@ -134,6 +139,22 @@
}
}
+#elif defined(XPCAgentSample)
+
+ NSData *data = nil;
+
+ if ([ws type:docType conformsToType:SKNPDFDocumentType] ||
+ [ws type:docType conformsToType:SKNPDFBundleDocumentType] ||
+ [ws type:docType conformsToType:SKNSkimNotesDocumentType]) {
+ data = [[SKNXPCSkimReader sharedReader] SkimNotesAtURL:absoluteURL];
+ if (data) {
+ @try { array = [NSKeyedUnarchiver unarchiveObjectWithData:data]; }
+ @catch (id e) {}
+ if (array == nil)
+ array = [NSPropertyListSerialization propertyListWithData:data
options:NSPropertyListImmutable format:NULL error:NULL];
+ }
+ }
+
#elif defined(ToolSample)
NSData *data = nil;
Modified: trunk/SkimNotes/SKNSkimReader.h
===================================================================
--- trunk/SkimNotes/SKNSkimReader.h 2023-11-24 14:49:15 UTC (rev 13795)
+++ trunk/SkimNotes/SKNSkimReader.h 2023-11-24 15:21:52 UTC (rev 13796)
@@ -48,11 +48,10 @@
id agent;
}
-+ (id)sharedReader;
+@property (class, nonatomic, readonly) id sharedReader;
-- (NSString *)agentIdentifier;
-// this should only be used before any of the following calls is made
-- (void)setAgentIdentifier:(NSString *)identifier;
+// this should only be set before any of the following calls is made
+@property (nonatomic, retain) NSString *agentIdentifier;
- (NSData *)SkimNotesAtURL:(NSURL *)fileURL;
- (NSData *)RTFNotesAtURL:(NSURL *)fileURL;
Modified: trunk/SkimNotes/SKNSkimReader.m
===================================================================
--- trunk/SkimNotes/SKNSkimReader.m 2023-11-24 14:49:15 UTC (rev 13795)
+++ trunk/SkimNotes/SKNSkimReader.m 2023-11-24 15:21:52 UTC (rev 13796)
@@ -43,6 +43,8 @@
@implementation SKNSkimReader
+@synthesize agentIdentifier;
+
+ (id)sharedReader {
static id sharedReader = nil;
if (nil == sharedReader)
@@ -72,10 +74,6 @@
[super dealloc];
}
-- (NSString *)agentIdentifier {
- return agentIdentifier;
-}
-
- (void)setAgentIdentifier:(NSString *)identifier {
NSAssert(connection == nil, @"agentIdentifier must be set before
connecting");
if (connection == nil && agentIdentifier != identifier) {
Added: trunk/SkimNotes/SKNXPCSkimReader.h
===================================================================
--- trunk/SkimNotes/SKNXPCSkimReader.h (rev 0)
+++ trunk/SkimNotes/SKNXPCSkimReader.h 2023-11-24 15:21:52 UTC (rev 13796)
@@ -0,0 +1,66 @@
+//
+// SKNXPCSkimReader.h
+// SkimNotesTest
+//
+// Created by Christiaan Hofman on 24/11/2023.
+/*
+ This software is Copyright (c) 2023
+ Christiaan Hofman. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ - Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ - 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.
+
+ - Neither the name of Adam Maxwell nor the names of any
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "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 THE COPYRIGHT
+ OWNER 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.
+ */
+
+#import <Cocoa/Cocoa.h>
+
+
+@interface SKNXPCSkimReader : NSObject {
+ NSString *agentIdentifier;
+ NSXPCConnection *connection;
+ id agent;
+ BOOL synchronous;
+}
+
++ (id)sharedReader;
+
+// this should only be set before any of the following calls is made
+@property (nonatomic, retain) NSString *agentIdentifier;
+
+// should use either the synchronous or the asynchronous methods, not both
+
+// synchronous retrieval
+- (NSData *)SkimNotesAtURL:(NSURL *)fileURL;
+- (NSData *)RTFNotesAtURL:(NSURL *)fileURL;
+- (NSString *)textNotesAtURL:(NSURL *)fileURL;
+
+// asynchronous retrieval
+- (void)readSkimNotesAtURL:(NSURL *)fileURL reply:(void (^)(NSData *))reply;
+- (void)readRTFNotesAtURL:(NSURL *)fileURL reply:(void (^)(NSData *))reply;
+- (void)readTextNotesAtURL:(NSURL *)fileURL reply:(void (^)(NSString *))reply;
+
+@end
Added: trunk/SkimNotes/SKNXPCSkimReader.m
===================================================================
--- trunk/SkimNotes/SKNXPCSkimReader.m (rev 0)
+++ trunk/SkimNotes/SKNXPCSkimReader.m 2023-11-24 15:21:52 UTC (rev 13796)
@@ -0,0 +1,239 @@
+//
+// SKNXPCSkimReader.m
+// SkimNotesTest
+//
+// Created by Christiaan Hofman on 24/11/2023.
+/*
+ This software is Copyright (c) 2023
+ Christiaan Hofman. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ - Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ - 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.
+
+ - Neither the name of Adam Maxwell nor the names of any
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "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 THE COPYRIGHT
+ OWNER 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.
+ */
+
+#import "SKNXPCSkimReader.h"
+#import "SKNXPCAgentListenerProtocol.h"
+#import <ServiceManagement/ServiceManagement.h>
+
+@implementation SKNXPCSkimReader
+
+@synthesize agentIdentifier;
+
++ (id)sharedReader {
+ static id sharedReader = nil;
+ if (nil == sharedReader)
+ sharedReader = [[self alloc] init];
+ return sharedReader;
+}
+
+- (void)destroyConnection {
+ [agent release];
+ agent = nil;
+
+ [connection invalidate];
+ [connection release];
+ connection = nil;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [self destroyConnection];
+ [agentIdentifier release];
+ [super dealloc];
+}
+
+- (void)setAgentIdentifier:(NSString *)identifier {
+ NSAssert(connection == nil, @"agentIdentifier must be set before
connecting");
+ if (connection == nil && agentIdentifier != identifier) {
+ [agentIdentifier release];
+ agentIdentifier = [identifier retain];
+ }
+}
+
+// this could be simplified
+- (NSString *)skimnotesToolPath {
+ // we assume the skimnotes tool is contained in the Resources of the main
bundle
+ NSString *path = [[NSBundle mainBundle] pathForResource:@"skimnotes"
ofType:nil];
+ if (path == nil) {
+ // in case this is included in a framework
+ path = [[NSBundle bundleForClass:[self class]]
pathForResource:@"skimnotes" ofType:nil];
+ }
+ if (path == nil) {
+ // look for it in the Skim bundle
+ NSString *SkimPath = [[NSWorkspace sharedWorkspace]
fullPathForApplication:@"Skim"];
+ path = SkimPath ? [[[NSBundle bundleWithPath:SkimPath]
sharedSupportPath] stringByAppendingPathComponent:@"skimnotes"] : nil;
+ }
+ return path;
+}
+
+- (BOOL)launchedTask {
+ BOOL taskLaunched = NO;
+ NSString *launchPath = [self skimnotesToolPath];
+
+ if (launchPath) {
+ NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
+
+ // can also use a fixed identifier, or let the tool decide about an
identifier and read it from stdout
+ if (nil == [self agentIdentifier])
+ [self setAgentIdentifier:[NSString
stringWithFormat:@"%@.skimnotes-%@@", bundleIdentifier, [[NSProcessInfo
processInfo] globallyUniqueString]]];
+
+ NSArray *arguments = @[launchPath, @"agent", @"-xpc", [self
agentIdentifier]];
+
+ AuthorizationRef auth = NULL;
+ OSStatus createStatus = AuthorizationCreate(NULL,
kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &auth);
+ if (createStatus != errAuthorizationSuccess) {
+ auth = NULL;
+ NSLog(@"failed to create authorization reference: %d",
createStatus);
+ }
+
+ Boolean submittedJob = false;
+ if (auth != NULL) {
+ NSString *label = [NSString
stringWithFormat:@"%@.skimnotes-agent", bundleIdentifier];
+
+ // Try to remove the job from launchd if it is already running
+ // We could invoke SMJobCopyDictionary() first to see if the job
exists, but I'd rather avoid
+ // using it because the headers indicate it may be removed one day
without any replacement
+ CFErrorRef removeError = NULL;
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ if (false == SMJobRemove(kSMDomainUserLaunchd,
(CFStringRef)(label), auth, true, &removeError)) {
+#pragma clang diagnostic pop
+ if (removeError != NULL) {
+ // It's normal for a job to not be found, so this is not
an interesting error
+ if (CFErrorGetCode(removeError) != kSMErrorJobNotFound)
+ NSLog(@"remove job error: %@", removeError);
+ CFRelease(removeError);
+ }
+ }
+
+ NSDictionary *jobDictionary = @{@"Label" : label,
@"ProgramArguments" : arguments, @"EnableTransactions" : @NO, @"KeepAlive" :
@{@"SuccessfulExit" : @NO}, @"RunAtLoad" : @NO, @"Nice" : @0, @"ProcessType":
@"Interactive", @"LaunchOnlyOnce": @YES, @"MachServices" : @{[self
agentIdentifier] : @YES}};
+
+ CFErrorRef submitError = NULL;
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ // SMJobSubmit is deprecated but is the only way to submit a
non-permanent
+ // helper and allows us to submit to user domain without requiring
authorization
+ submittedJob = SMJobSubmit(kSMDomainUserLaunchd,
(CFDictionaryRef)(jobDictionary), auth, &submitError);
+#pragma clang diagnostic pop
+ if (submittedJob == false) {
+ if (submitError != NULL) {
+ NSLog(@"submit job error: %@", submitError);
+ CFRelease(submitError);
+ }
+ } else {
+ taskLaunched = YES;
+ }
+ }
+
+ if (auth != NULL)
+ AuthorizationFree(auth, kAuthorizationFlagDefaults);
+ } else {
+ NSLog(@"failed to find skimnotes tool");
+ }
+ return taskLaunched;
+}
+
+- (void)establishSynchronousConnection:(BOOL)sync {
+ if ([self launchedTask]) {
+ connection = [[NSXPCConnection alloc] initWithMachServiceName:[self
agentIdentifier] options:0];
+ [connection setRemoteObjectInterface:[NSXPCInterface
interfaceWithProtocol:@protocol(SKNXPCAgentListenerProtocol)]];
+ [connection setInvalidationHandler:^{
+ [self destroyConnection];
+ }];
+ if (sync)
+ agent = [[connection
synchronousRemoteObjectProxyWithErrorHandler:^(NSError *error){}] retain];
+ else
+ agent = [[connection remoteObjectProxy] retain];
+ synchronous = sync;
+ }
+}
+
+- (BOOL)connectAndCheckTypeOfFile:(NSURL *)fileURL synchronous:(BOOL)sync {
+ if (nil == connection) {
+ [self establishSynchronousConnection:sync];
+ } else if (synchronous != sync) {
+ NSLog(@"attempt to mix synxhronous and asynchronous skim notes
retrieval");
+ return NO;
+ }
+
+ // these checks are client side to avoid connecting to the server unless
it's really necessary
+ NSWorkspace *ws = [NSWorkspace sharedWorkspace];
+ NSString *fileType = [ws typeOfFile:[fileURL path] error:NULL];
+
+ if (fileType != nil &&
+ ([ws type:fileType conformsToType:(NSString *)kUTTypePDF] ||
+ [ws type:fileType conformsToType:@"net.sourceforge.skim-app.pdfd"] ||
+ [ws type:fileType
conformsToType:@"net.sourceforge.skim-app.skimnotes"]))
+ return YES;
+
+ return NO;
+}
+
+- (NSData *)SkimNotesAtURL:(NSURL *)fileURL {
+ __block NSData *data = nil;
+ if ([self connectAndCheckTypeOfFile:fileURL synchronous:YES])
+ [agent readSkimNotesAtURL:fileURL reply:^(NSData *d){ data = [[d
retain] autorelease]; }];
+ return data;
+}
+
+- (NSData *)RTFNotesAtURL:(NSURL *)fileURL {
+ __block NSData *data = nil;
+ if ([self connectAndCheckTypeOfFile:fileURL synchronous:YES])
+ [agent readRTFNotesAtURL:fileURL reply:^(NSData *d){ data = [[d
retain] autorelease]; }];
+ return data;
+}
+
+- (NSString *)textNotesAtURL:(NSURL *)fileURL {
+ __block NSString *string = nil;
+ if ([self connectAndCheckTypeOfFile:fileURL synchronous:YES])
+ [agent readTextNotesAtURL:fileURL reply:^(NSString *s){ string = [s
retain]; }];
+ return string;
+}
+
+- (void)readSkimNotesAtURL:(NSURL *)fileURL reply:(void (^)(NSData *))reply {
+ if ([self connectAndCheckTypeOfFile:fileURL synchronous:NO])
+ [agent readSkimNotesAtURL:fileURL reply:reply];
+ else
+ reply(nil);
+}
+
+- (void)readRTFNotesAtURL:(NSURL *)fileURL reply:(void (^)(NSData *))reply {
+ if ([self connectAndCheckTypeOfFile:fileURL synchronous:NO])
+ [agent readRTFNotesAtURL:fileURL reply:reply];
+ else
+ reply(nil);
+}
+
+- (void)readTextNotesAtURL:(NSURL *)fileURL reply:(void (^)(NSString *))reply {
+ if ([self connectAndCheckTypeOfFile:fileURL synchronous:NO])
+ [agent readTextNotesAtURL:fileURL reply:reply];
+ else
+ reply(nil);
+}
+
+@end
Modified: trunk/SkimNotes/SkimNotes.xcodeproj/project.pbxproj
===================================================================
--- trunk/SkimNotes/SkimNotes.xcodeproj/project.pbxproj 2023-11-24 14:49:15 UTC
(rev 13795)
+++ trunk/SkimNotes/SkimNotes.xcodeproj/project.pbxproj 2023-11-24 15:21:52 UTC
(rev 13796)
@@ -120,6 +120,7 @@
CEDB9B040E2EA0200057FD09 /* skimnotes in Resources */ = {isa =
PBXBuildFile; fileRef = CEBA2B550E0566B00000B2E6 /* skimnotes */; };
CEDB9B060E2EA0450057FD09 /* Cocoa.framework in Frameworks */ =
{isa = PBXBuildFile; fileRef = 1058C7B1FEA5585E11CA2CBB /* Cocoa.framework */;
};
CEDB9B210E2EA2F30057FD09 /* main.m in Sources */ = {isa =
PBXBuildFile; fileRef = CEDB9B200E2EA2F30057FD09 /* main.m */; };
+ CEED30562B10B02C003F138F /* SKNXPCSkimReader.m in Sources */ =
{isa = PBXBuildFile; fileRef = CEED30552B10B02C003F138F /* SKNXPCSkimReader.m
*/; };
CEEEEFFE2996564500A0D98F /* Quartz.framework in Frameworks */ =
{isa = PBXBuildFile; fileRef = CEF57FD92988180700594EC0 /* Quartz.framework */;
};
CEEEEFFF2996564F00A0D98F /* Quartz.framework in Frameworks */ =
{isa = PBXBuildFile; fileRef = CEF57FD92988180700594EC0 /* Quartz.framework */;
};
CEEEF0012996568E00A0D98F /* Quartz.framework in Frameworks */ =
{isa = PBXBuildFile; fileRef = CEF57FD92988180700594EC0 /* Quartz.framework */;
};
@@ -269,6 +270,8 @@
CEDB9B0D0E2EA1380057FD09 /* SkimNotes-App.xcconfig */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path =
"SkimNotes-App.xcconfig"; sourceTree = "<group>"; };
CEDB9B200E2EA2F30057FD09 /* main.m */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path
= main.m; sourceTree = "<group>"; };
CEED30532B10AE38003F138F /* SKNXPCAgentListenerProtocol.h */ =
{isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path =
SKNXPCAgentListenerProtocol.h; sourceTree = "<group>"; };
+ CEED30542B10B02C003F138F /* SKNXPCSkimReader.h */ = {isa =
PBXFileReference; lastKnownFileType = sourcecode.c.h; path =
SKNXPCSkimReader.h; sourceTree = "<group>"; };
+ CEED30552B10B02C003F138F /* SKNXPCSkimReader.m */ = {isa =
PBXFileReference; lastKnownFileType = sourcecode.c.objc; path =
SKNXPCSkimReader.m; sourceTree = "<group>"; };
CEF57FCF2988170B00594EC0 /* PDFKit.framework */ = {isa =
PBXFileReference; lastKnownFileType = wrapper.framework; name =
PDFKit.framework; path = System/Library/Frameworks/PDFKit.framework; sourceTree
= SDKROOT; };
CEF57FD12988171C00594EC0 /* Foundation.framework */ = {isa =
PBXFileReference; lastKnownFileType = wrapper.framework; name =
Foundation.framework; path = System/Library/Frameworks/Foundation.framework;
sourceTree = SDKROOT; };
CEF57FD32988172600594EC0 /* AppKit.framework */ = {isa =
PBXFileReference; lastKnownFileType = wrapper.framework; name =
AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree
= SDKROOT; };
@@ -520,6 +523,8 @@
CEDB9AC40E2E9D760057FD09 /* SKNDocument.m */,
CEDB9AC50E2E9D760057FD09 /* SKNSkimReader.h */,
CEDB9AC60E2E9D760057FD09 /* SKNSkimReader.m */,
+ CEED30542B10B02C003F138F /* SKNXPCSkimReader.h
*/,
+ CEED30552B10B02C003F138F /* SKNXPCSkimReader.m
*/,
);
name = "Test App Classes";
sourceTree = "<group>";
@@ -937,6 +942,7 @@
CEDB9AF40E2E9F250057FD09 /* SKNDocument.m in
Sources */,
CEDB9AF50E2E9F260057FD09 /* SKNSkimReader.m in
Sources */,
CEDB9B210E2EA2F30057FD09 /* main.m in Sources
*/,
+ CEED30562B10B02C003F138F /* SKNXPCSkimReader.m
in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
This was sent by the SourceForge.net collaborative development platform, the
world's largest Open Source development site.
_______________________________________________
Skim-app-commit mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/skim-app-commit