diff --git a/Configuration/Base.xcconfig b/Configuration/Base.xcconfig index 893c8c9..a88d1fe 100644 --- a/Configuration/Base.xcconfig +++ b/Configuration/Base.xcconfig @@ -6,7 +6,7 @@ #include "Version.xcconfig" DEVELOPMENT_TEAM = -PRODUCT_BUNDLE_IDENTIFIER = com.lookinside-app.lookinside +PRODUCT_BUNDLE_IDENTIFIER = cn.vanjay.lookinside CODE_SIGN_STYLE = Automatic SPARKLE_PUBLIC_ED_KEY = diff --git a/LookInside.xcodeproj/project.pbxproj b/LookInside.xcodeproj/project.pbxproj index e9fb792..299d5d4 100644 --- a/LookInside.xcodeproj/project.pbxproj +++ b/LookInside.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ E37B8D0D2F97B1B100600001 /* Demangling in Frameworks */ = {isa = PBXBuildFile; productRef = E37B8D0C2F97B1B100600001 /* Demangling */; }; E37B8D0F2F97B1B100600001 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E37B8D0F1F97B1B100600001 /* Sparkle */; }; E37B8D222F97B1B100600001 /* LookInsidePrivateDiscriminator in Frameworks */ = {isa = PBXBuildFile; productRef = E37B8D212F97B1B100600001 /* LookInsidePrivateDiscriminator */; }; + E5B000012FA9000000000001 /* LookinCore in Frameworks */ = {isa = PBXBuildFile; productRef = E5B000032FA9000000000001 /* LookinCore */; }; E97210062FC2D5C0003AAEA6 /* RunningApplicationKit in Frameworks */ = {isa = PBXBuildFile; productRef = E97210052FC2D5C0003AAEA6 /* RunningApplicationKit */; }; E97210092FC2E5C0003AAEA6 /* HelperClient in Frameworks */ = {isa = PBXBuildFile; productRef = E97210082FC2E5C0003AAEA6 /* HelperClient */; }; E972100B2FC2E5C0003AAEA6 /* HelperCommunication in Frameworks */ = {isa = PBXBuildFile; productRef = E972100A2FC2E5C0003AAEA6 /* HelperCommunication */; }; @@ -21,7 +22,7 @@ /* Begin PBXFileReference section */ AA1CAA3D22E471EC006D9285 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; - AA48F29B21CC0CA50032EC2C /* LookInside.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LookInside.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AA48F29B21CC0CA50032EC2C /* LookInside 酷狗版.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "LookInside 酷狗版.app"; sourceTree = BUILT_PRODUCTS_DIR; }; AAB9517A22C3AECA00A5F958 /* Quartz.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quartz.framework; path = System/Library/Frameworks/Quartz.framework; sourceTree = SDKROOT; }; AAB9519122C3BDC900A5F958 /* QuickLookThumbnailing.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookThumbnailing.framework; path = System/Library/Frameworks/QuickLookThumbnailing.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -146,42 +147,9 @@ Dashboard/Search/LKDashboardSearchMethodsDataSource.m, Dashboard/Search/LKDashboardSearchMethodsView.m, Dashboard/Search/LKDashboardSearchPropView.m, - "DerivedSource/LookinCore/Category/CALayer+Lookin.m", - "DerivedSource/LookinCore/Category/Color+Lookin.m", - "DerivedSource/LookinCore/Category/Image+Lookin.m", - "DerivedSource/LookinCore/Category/NSArray+Lookin.m", - "DerivedSource/LookinCore/Category/NSObject+Lookin.m", - "DerivedSource/LookinCore/Category/NSSet+Lookin.m", - "DerivedSource/LookinCore/Category/NSString+Lookin.m", - "DerivedSource/LookinCore/Category/NSValue+Lookin.m", - DerivedSource/LookinCore/LKS_MultiplatformAdapter.m, - DerivedSource/LookinCore/LookinAppInfo.m, - DerivedSource/LookinCore/LookinAttribute.m, - DerivedSource/LookinCore/LookinAttributeModification.m, - DerivedSource/LookinCore/LookinAttributesGroup.m, - DerivedSource/LookinCore/LookinAttributesSection.m, - DerivedSource/LookinCore/LookinAttrIdentifiers.m, - DerivedSource/LookinCore/LookinAutoLayoutConstraint.m, - DerivedSource/LookinCore/LookinConnectionAttachment.m, - DerivedSource/LookinCore/LookinConnectionResponseAttachment.m, - DerivedSource/LookinCore/LookinCustomAttrModification.m, - DerivedSource/LookinCore/LookinCustomDisplayItemInfo.m, - DerivedSource/LookinCore/LookinDashboardBlueprint.m, - DerivedSource/LookinCore/LookinDisplayItem.m, - DerivedSource/LookinCore/LookinDisplayItemDetail.m, - DerivedSource/LookinCore/LookinEventHandler.m, - DerivedSource/LookinCore/LookinHierarchyFile.m, - DerivedSource/LookinCore/LookinHierarchyInfo.m, - DerivedSource/LookinCore/LookinObject.m, - DerivedSource/LookinCore/LookinStaticAsyncUpdateTask.m, - DerivedSource/LookinCore/LookinTuple.m, - DerivedSource/LookinCore/LookinWeakContainer.m, - DerivedSource/LookinCore/Peertalk/Lookin_PTChannel.m, - DerivedSource/LookinCore/Peertalk/Lookin_PTProtocol.m, - DerivedSource/LookinCore/Peertalk/Lookin_PTUSBHub.m, - DerivedSource/LookinServerBase/LookinIvarTrace.m, Export/LKExportAccessoryView.m, Export/LKExportManager.m, + Export/LKMCPNodeExporter.m, Hierarchy/LKDanceUIAttrMaker.m, Hierarchy/LKHierarchyController.m, Hierarchy/LKHierarchyDataSource.m, @@ -441,48 +409,9 @@ Dashboard/Search/LKDashboardSearchMethodsDataSource.h, Dashboard/Search/LKDashboardSearchMethodsView.h, Dashboard/Search/LKDashboardSearchPropView.h, - "DerivedSource/LookinCore/Category/CALayer+Lookin.h", - "DerivedSource/LookinCore/Category/Color+Lookin.h", - "DerivedSource/LookinCore/Category/Image+Lookin.h", - "DerivedSource/LookinCore/Category/NSArray+Lookin.h", - "DerivedSource/LookinCore/Category/NSObject+Lookin.h", - "DerivedSource/LookinCore/Category/NSSet+Lookin.h", - "DerivedSource/LookinCore/Category/NSString+Lookin.h", - "DerivedSource/LookinCore/Category/NSValue+Lookin.h", - DerivedSource/LookinCore/include/LookinCore.h, - DerivedSource/LookinCore/LKS_MultiplatformAdapter.h, - DerivedSource/LookinCore/LookinAppInfo.h, - DerivedSource/LookinCore/LookinAttribute.h, - DerivedSource/LookinCore/LookinAttributeModification.h, - DerivedSource/LookinCore/LookinAttributesGroup.h, - DerivedSource/LookinCore/LookinAttributesSection.h, - DerivedSource/LookinCore/LookinAttrIdentifiers.h, - DerivedSource/LookinCore/LookinAttrType.h, - DerivedSource/LookinCore/LookinAutoLayoutConstraint.h, - DerivedSource/LookinCore/LookinCodingValueType.h, - DerivedSource/LookinCore/LookinConnectionAttachment.h, - DerivedSource/LookinCore/LookinConnectionResponseAttachment.h, - DerivedSource/LookinCore/LookinCustomAttrModification.h, - DerivedSource/LookinCore/LookinCustomDisplayItemInfo.h, - DerivedSource/LookinCore/LookinDashboardBlueprint.h, - DerivedSource/LookinCore/LookinDefines.h, - DerivedSource/LookinCore/LookinDisplayItem.h, - DerivedSource/LookinCore/LookinDisplayItemDetail.h, - DerivedSource/LookinCore/LookinEventHandler.h, - DerivedSource/LookinCore/LookinHierarchyFile.h, - DerivedSource/LookinCore/LookinHierarchyInfo.h, - DerivedSource/LookinCore/LookinObject.h, - DerivedSource/LookinCore/LookinStaticAsyncUpdateTask.h, - DerivedSource/LookinCore/LookinTuple.h, - DerivedSource/LookinCore/LookinWeakContainer.h, - DerivedSource/LookinCore/Peertalk/Lookin_PTChannel.h, - DerivedSource/LookinCore/Peertalk/Lookin_PTPrivate.h, - DerivedSource/LookinCore/Peertalk/Lookin_PTProtocol.h, - DerivedSource/LookinCore/Peertalk/Lookin_PTUSBHub.h, - DerivedSource/LookinCore/Peertalk/Peertalk.h, - DerivedSource/LookinServerBase/LookinIvarTrace.h, Export/LKExportAccessoryView.h, Export/LKExportManager.h, + Export/LKMCPNodeExporter.h, Hierarchy/LKDanceUIAttrMaker.h, Hierarchy/LKHierarchyController.h, Hierarchy/LKHierarchyDataSource.h, @@ -645,6 +574,7 @@ 50EAE1AC2F71C01A00EBE063 /* QuickLookThumbnailing.framework in Frameworks */, E37B8D0D2F97B1B100600001 /* Demangling in Frameworks */, E37B8D0F2F97B1B100600001 /* Sparkle in Frameworks */, + E5B000012FA9000000000001 /* LookinCore in Frameworks */, E97210062FC2D5C0003AAEA6 /* RunningApplicationKit in Frameworks */, E37B8D222F97B1B100600001 /* LookInsidePrivateDiscriminator in Frameworks */, E97210092FC2E5C0003AAEA6 /* HelperClient in Frameworks */, @@ -679,7 +609,7 @@ AA48F29C21CC0CA50032EC2C /* Products */ = { isa = PBXGroup; children = ( - AA48F29B21CC0CA50032EC2C /* LookInside.app */, + AA48F29B21CC0CA50032EC2C /* LookInside 酷狗版.app */, ); name = Products; sourceTree = ""; @@ -705,6 +635,7 @@ packageProductDependencies = ( E37B8D0C2F97B1B100600001 /* Demangling */, E37B8D0F1F97B1B100600001 /* Sparkle */, + E5B000032FA9000000000001 /* LookinCore */, E37B8D212F97B1B100600001 /* LookInsidePrivateDiscriminator */, E97210052FC2D5C0003AAEA6 /* RunningApplicationKit */, E97210082FC2E5C0003AAEA6 /* HelperClient */, @@ -712,7 +643,7 @@ E972100C2FC2E5C0003AAEA6 /* InjectionServiceInterface */, ); productName = Lookin; - productReference = AA48F29B21CC0CA50032EC2C /* LookInside.app */; + productReference = AA48F29B21CC0CA50032EC2C /* LookInside 酷狗版.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -750,6 +681,7 @@ ); mainGroup = AA48F29221CC0CA50032EC2C; packageReferences = ( + E5B000022FA9000000000001 /* XCLocalSwiftPackageReference "." */, E37B8D0B2F97B1B100600001 /* XCRemoteSwiftPackageReference "swift-demangling" */, E37B8D0F0F97B1B100600001 /* XCRemoteSwiftPackageReference "Sparkle" */, E37B8D202F97B1B100600001 /* XCRemoteSwiftPackageReference "LookInsidePrivateDiscriminator" */, @@ -813,7 +745,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [ ! -d \"$SRCROOT/../LookInside-Injector\" ]; then\n if [ \"$CONFIGURATION\" = \"Release\" ]; then\n echo \"error: LookInside-Injector not present; Release builds must embed the injector daemon\"\n exit 1\n fi\n echo \"Skipping Embed Injector Daemon: LookInside-Injector not present (not in monorepo checkout)\"\n exit 0\nfi\nPREPARE_SCRIPT=\"$SRCROOT/Scripts/prepare-injector-daemon.sh\"\n# Run in a fresh login zsh so xcodebuild env from the host build does not leak\n# into the nested LookInside-Injector build (would crash XCBBuildService).\n# Re-export the host's BUILT_PRODUCTS_DIR / CONTENTS_FOLDER_PATH / CONFIGURATION\n# so the nested zsh can locate the LookInside.app bundle to embed into.\nenv -i HOME=\"$HOME\" USER=\"$USER\" TERM=\"${TERM:-dumb}\" \\\n GITHUB_TOKEN=\"${GITHUB_TOKEN:-}\" \\\n CONFIGURATION=\"$CONFIGURATION\" \\\n BUILT_PRODUCTS_DIR=\"$BUILT_PRODUCTS_DIR\" \\\n CONTENTS_FOLDER_PATH=\"$CONTENTS_FOLDER_PATH\" \\\n /bin/zsh -lc \"$PREPARE_SCRIPT\"\n"; + shellScript = "if [ ! -d \"$SRCROOT/../LookInside-Injector\" ]; then\n if [ \"$CONFIGURATION\" = \"Release\" ] && [ \"${LOOKINSIDE_ALLOW_MISSING_INJECTOR:-NO}\" != \"YES\" ]; then\n echo \"error: LookInside-Injector not present; Release builds must embed the injector daemon\"\n exit 1\n fi\n echo \"Skipping Embed Injector Daemon: LookInside-Injector not present (not in monorepo checkout)\"\n exit 0\nfi\nPREPARE_SCRIPT=\"$SRCROOT/Scripts/prepare-injector-daemon.sh\"\n# Run in a fresh login zsh so xcodebuild env from the host build does not leak\n# into the nested LookInside-Injector build (would crash XCBBuildService).\n# Re-export the host's BUILT_PRODUCTS_DIR / CONTENTS_FOLDER_PATH / CONFIGURATION\n# so the nested zsh can locate the LookInside.app bundle to embed into.\nenv -i HOME=\"$HOME\" USER=\"$USER\" TERM=\"${TERM:-dumb}\" \\\n GITHUB_TOKEN=\"${GITHUB_TOKEN:-}\" \\\n CONFIGURATION=\"$CONFIGURATION\" \\\n BUILT_PRODUCTS_DIR=\"$BUILT_PRODUCTS_DIR\" \\\n CONTENTS_FOLDER_PATH=\"$CONTENTS_FOLDER_PATH\" \\\n /bin/zsh -lc \"$PREPARE_SCRIPT\"\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -990,6 +922,7 @@ CURRENT_PROJECT_VERSION = "$(inherited)"; DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = X6B6C6U6QV; ENABLE_APP_SANDBOX = NO; ENABLE_ENHANCED_SECURITY = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -1019,7 +952,7 @@ "$(SRCROOT)/LookInside/ReactiveObjC", ); INFOPLIST_FILE = LookInside/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = LookInside; + INFOPLIST_KEY_CFBundleDisplayName = "LookInside 酷狗版"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_OUTPUT_FORMAT = XML; LD_RUNPATH_SEARCH_PATHS = ( @@ -1030,7 +963,9 @@ MARKETING_VERSION = "$(inherited)"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; PRODUCT_BUNDLE_IDENTIFIER = "$(inherited)"; - PRODUCT_NAME = LookInside; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = cn.vanjay.lookinside; + PRODUCT_MODULE_NAME = LookInside; + PRODUCT_NAME = "LookInside 酷狗版"; PROVISIONING_PROFILE_SPECIFIER = "$(inherited)"; RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = YES; RUNTIME_EXCEPTION_ALLOW_JIT = YES; @@ -1039,6 +974,7 @@ RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = YES; RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = YES; SWIFT_OBJC_BRIDGING_HEADER = "LookInside/Base/LookinClient-Bridging-Header.h"; + SWIFT_OBJC_INTERFACE_HEADER_NAME = "LookInside-Swift.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; WARNING_CFLAGS = ( @@ -1064,6 +1000,7 @@ CURRENT_PROJECT_VERSION = "$(inherited)"; DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = X6B6C6U6QV; ENABLE_APP_SANDBOX = NO; ENABLE_ENHANCED_SECURITY = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -1093,7 +1030,7 @@ "$(SRCROOT)/LookInside/ReactiveObjC", ); INFOPLIST_FILE = LookInside/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = LookInside; + INFOPLIST_KEY_CFBundleDisplayName = "LookInside 酷狗版"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_OUTPUT_FORMAT = XML; LD_RUNPATH_SEARCH_PATHS = ( @@ -1104,7 +1041,9 @@ MARKETING_VERSION = "$(inherited)"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; PRODUCT_BUNDLE_IDENTIFIER = "$(inherited)"; - PRODUCT_NAME = LookInside; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = cn.vanjay.lookinside; + PRODUCT_MODULE_NAME = LookInside; + PRODUCT_NAME = "LookInside 酷狗版"; PROVISIONING_PROFILE_SPECIFIER = "$(inherited)"; RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = YES; RUNTIME_EXCEPTION_ALLOW_JIT = YES; @@ -1113,6 +1052,7 @@ RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = YES; RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = YES; SWIFT_OBJC_BRIDGING_HEADER = "LookInside/Base/LookinClient-Bridging-Header.h"; + SWIFT_OBJC_INTERFACE_HEADER_NAME = "LookInside-Swift.h"; SWIFT_VERSION = 5.0; WARNING_CFLAGS = ( "$(inherited)", @@ -1144,6 +1084,13 @@ }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + E5B000022FA9000000000001 /* XCLocalSwiftPackageReference "." */ = { + isa = XCLocalSwiftPackageReference; + relativePath = .; + }; +/* End XCLocalSwiftPackageReference section */ + /* Begin XCRemoteSwiftPackageReference section */ E37B8D0B2F97B1B100600001 /* XCRemoteSwiftPackageReference "swift-demangling" */ = { isa = XCRemoteSwiftPackageReference; @@ -1203,6 +1150,11 @@ package = E37B8D202F97B1B100600001 /* XCRemoteSwiftPackageReference "LookInsidePrivateDiscriminator" */; productName = LookInsidePrivateDiscriminator; }; + E5B000032FA9000000000001 /* LookinCore */ = { + isa = XCSwiftPackageProductDependency; + package = E5B000022FA9000000000001 /* XCLocalSwiftPackageReference "." */; + productName = LookinCore; + }; E97210052FC2D5C0003AAEA6 /* RunningApplicationKit */ = { isa = XCSwiftPackageProductDependency; package = E97210042FC2D5C0003AAEA6 /* XCRemoteSwiftPackageReference "RunningApplicationKit" */; diff --git a/LookInside.xcodeproj/xcshareddata/xcschemes/LookInside.xcscheme b/LookInside.xcodeproj/xcshareddata/xcschemes/LookInside.xcscheme index 1a2d2fc..05206ee 100644 --- a/LookInside.xcodeproj/xcshareddata/xcschemes/LookInside.xcscheme +++ b/LookInside.xcodeproj/xcshareddata/xcschemes/LookInside.xcscheme @@ -17,7 +17,7 @@ @@ -35,7 +35,7 @@ @@ -64,7 +64,7 @@ @@ -81,7 +81,7 @@ diff --git a/LookInside.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LookInside.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0d364b5..77331df 100644 --- a/LookInside.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LookInside.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "fa3ea308a0e3fba099f2a1ff13562912b7a5fa2d67a25f3716fb800d589374b8", + "originHash" : "609d337a7256229bd0ca908f624d35d636d5a81b36a8fc9075aaf695826c5a4e", "pins" : [ + { + "identity" : "eventsource", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattt/eventsource.git", + "state" : { + "revision" : "a3a85a85214caf642abaa96ae664e4c772a59f6e", + "version" : "1.4.1" + } + }, { "identity" : "frameworktoolbox", "kind" : "remoteSourceControl", @@ -46,6 +55,24 @@ "version" : "2.9.1" } }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", + "version" : "1.6.0" + } + }, { "identity" : "swift-demangling", "kind" : "remoteSourceControl", @@ -64,6 +91,33 @@ "version" : "0.1.3" } }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "92448c359f00ebe36ae97d3bd9086f13c7692b5a", + "version" : "1.13.2" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "77b84ac2cd2ac9e4ac67d19f045fd5b434f56967", + "version" : "2.101.0" + } + }, + { + "identity" : "swift-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/modelcontextprotocol/swift-sdk.git", + "state" : { + "revision" : "a0ae212ebf6eab5f754c3129608bc5557637e605", + "version" : "0.12.1" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", @@ -73,6 +127,15 @@ "version" : "601.0.1" } }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "7502b711c92a17741fa625d722b0ccbd595d8ed1", + "version" : "1.7.2" + } + }, { "identity" : "swiftyxpc", "kind" : "remoteSourceControl", diff --git a/LookInside/Export/LKMCPNodeExporter.h b/LookInside/Export/LKMCPNodeExporter.h new file mode 100644 index 0000000..35cdddb --- /dev/null +++ b/LookInside/Export/LKMCPNodeExporter.h @@ -0,0 +1,42 @@ +// +// LKMCPNodeExporter.h +// Lookin +// +// Exports a LookinDisplayItem (and optionally its whole subtree) into the same +// JSON shape the `lookinside-mcp` tools emit (see Sources/LookinMCPCore/JSONShape.swift: +// oid / className / role / frame / bounds / alpha / hidden / text / +// accessibilityIdentifier / accessibilityLabel / path / children), enriched with the +// full per-node `attributes` groups exactly as the GUI dashboard panel shows them. +// +// The macOS client does NOT link the Swift MCP target, so this is a faithful ObjC +// re-implementation of the same schema. Keep the field names in sync with JSONShape.Node. +// + +#import + +@class LookinDisplayItem; + +NS_ASSUME_NONNULL_BEGIN + +@interface LKMCPNodeExporter : NSObject + +/// Build the MCP-format dictionary for `item`. When `recursive` is YES the returned +/// dictionary's `children` key holds the full subtree; when NO, `children` is an empty array. ++ (NSDictionary *)mcpDictionaryForItem:(LookinDisplayItem *)item recursive:(BOOL)recursive; + +/// Pretty-printed UTF-8 JSON data for `mcpDictionaryForItem:recursive:`. ++ (nullable NSData *)jsonDataForItem:(LookinDisplayItem *)item + recursive:(BOOL)recursive + error:(NSError *_Nullable *_Nullable)error; + +/// A suggested file name (without directory) for the exported JSON, e.g. +/// `UILabel_140234_analysis.json` or `UIView_140234_analysis_tree.json`. ++ (NSString *)suggestedFileNameForItem:(LookinDisplayItem *)item recursive:(BOOL)recursive; + +/// Human-readable text of the node's dashboard attribute data (grouped, "Title: value"), +/// suitable for placing on the pasteboard. Mirrors what the GUI panel displays. ++ (NSString *)readableAttributesTextForItem:(LookinDisplayItem *)item; + +@end + +NS_ASSUME_NONNULL_END diff --git a/LookInside/Export/LKMCPNodeExporter.m b/LookInside/Export/LKMCPNodeExporter.m new file mode 100644 index 0000000..293c9d8 --- /dev/null +++ b/LookInside/Export/LKMCPNodeExporter.m @@ -0,0 +1,440 @@ +// +// LKMCPNodeExporter.m +// Lookin +// + +#import "LKMCPNodeExporter.h" +#import +#import "LookinDisplayItem.h" +#import "LookinDisplayItem+LookinClient.h" +#import "LookinObject.h" +#import "LookinAttributesGroup.h" +#import "LookinAttributesGroup+LookinClient.h" +#import "LookinAttributesSection.h" +#import "LookinAttribute.h" +#import "LookinAttrType.h" +#import "LookinDashboardBlueprint.h" + +@implementation LKMCPNodeExporter + +#pragma mark - Public + ++ (NSDictionary *)mcpDictionaryForItem:(LookinDisplayItem *)item recursive:(BOOL)recursive { + if (!item) { + return @{}; + } + return [self _nodeDictForItem:item recursive:recursive]; +} + ++ (NSData *)jsonDataForItem:(LookinDisplayItem *)item recursive:(BOOL)recursive error:(NSError **)error { + NSDictionary *dict = [self mcpDictionaryForItem:item recursive:recursive]; + if (![NSJSONSerialization isValidJSONObject:dict]) { + if (error) { + *error = [NSError errorWithDomain:@"LKMCPNodeExporter" + code:-1 + userInfo:@{NSLocalizedDescriptionKey: @"Generated object is not valid JSON."}]; + } + return nil; + } + return [NSJSONSerialization dataWithJSONObject:dict + options:(NSJSONWritingPrettyPrinted | NSJSONWritingSortedKeys) + error:error]; +} + ++ (NSString *)suggestedFileNameForItem:(LookinDisplayItem *)item recursive:(BOOL)recursive { + NSString *className = [self _classNameForItem:item] ?: @"Node"; + // Keep only filename-safe characters. + NSCharacterSet *illegal = [NSCharacterSet characterSetWithCharactersInString:@"/\\:*?\"<>|"]; + className = [[className componentsSeparatedByCharactersInSet:illegal] componentsJoinedByString:@"_"]; + unsigned long oid = [self _oidForItem:item]; + NSString *suffix = recursive ? @"analysis_tree" : @"analysis"; + if (oid) { + return [NSString stringWithFormat:@"%@_%lu_%@.json", className, oid, suffix]; + } + return [NSString stringWithFormat:@"%@_%@.json", className, suffix]; +} + ++ (NSString *)readableAttributesTextForItem:(LookinDisplayItem *)item { + if (!item) { + return @""; + } + NSMutableString *text = [NSMutableString string]; + + NSString *className = [self _classNameForItem:item] ?: @""; + unsigned long oid = [self _oidForItem:item]; + if (oid) { + [text appendFormat:@"%@ (oid: %lu)\n", className, oid]; + } else { + [text appendFormat:@"%@\n", className]; + } + NSString *path = [self _breadcrumbForItem:item]; + if (path.length) { + [text appendFormat:@"path: %@\n", path]; + } + + NSArray *groups = [item queryAllAttrGroupList]; + for (LookinAttributesGroup *group in groups) { + NSString *groupTitle = [self _titleForGroup:group]; + [text appendFormat:@"\n[%@]\n", groupTitle.length ? groupTitle : @"-"]; + + for (LookinAttributesSection *section in group.attrSections) { + NSString *sectionTitle = [self _titleForSection:section]; + if (sectionTitle.length) { + [text appendFormat:@" %@\n", sectionTitle]; + } + for (LookinAttribute *attr in section.attributes) { + NSString *attrTitle = [self _titleForAttribute:attr]; + NSString *valueString = [self _readableStringForAttribute:attr]; + NSString *indent = sectionTitle.length ? @" " : @" "; + if (attrTitle.length) { + [text appendFormat:@"%@%@: %@\n", indent, attrTitle, valueString]; + } else { + [text appendFormat:@"%@%@\n", indent, valueString]; + } + } + } + } + return text.copy; +} + +#pragma mark - Node assembly (MCP JSONShape.Node parity) + ++ (NSDictionary *)_nodeDictForItem:(LookinDisplayItem *)item recursive:(BOOL)recursive { + NSMutableDictionary *node = [NSMutableDictionary dictionary]; + + node[@"oid"] = @([self _oidForItem:item]); + NSString *className = [self _classNameForItem:item] ?: @"UnknownView"; + node[@"className"] = className; + NSString *role = [self _roleForClassName:className]; + node[@"role"] = role ?: [NSNull null]; + node[@"frame"] = [self _rectDict:item.frame]; + node[@"bounds"] = [self _rectDict:item.bounds]; + node[@"alpha"] = [self _num:item.alpha]; + node[@"hidden"] = @(item.isHidden); + + NSString *text = [self _extractStringAttributeForItem:item identifiers:@[@"text", @"title", @"stringValue", @"attributedText"]]; + node[@"text"] = text.length ? text : [NSNull null]; + + NSString *a11yID = [self _extractStringAttributeForItem:item identifiers:@[@"accessibilityIdentifier"]]; + node[@"accessibilityIdentifier"] = a11yID.length ? a11yID : [NSNull null]; + + NSString *a11yLabel = [self _extractStringAttributeForItem:item identifiers:@[@"accessibilityLabel"]]; + node[@"accessibilityLabel"] = a11yLabel.length ? a11yLabel : [NSNull null]; + + NSString *path = [self _breadcrumbForItem:item]; + node[@"path"] = path.length ? path : [NSNull null]; + + // Full per-node attribute groups exactly as the dashboard panel renders them. + node[@"attributes"] = [self _attributeGroupsArrayForItem:item]; + + // Children. + NSMutableArray *children = [NSMutableArray array]; + if (recursive) { + for (LookinDisplayItem *sub in item.subitems) { + [children addObject:[self _nodeDictForItem:sub recursive:YES]]; + } + } + node[@"children"] = children; + + return node; +} + +/// JSON-safe number: non-finite (NaN/±Inf) values become NSNull so serialization can't fail. ++ (id)_num:(double)v { + return isfinite(v) ? @(v) : (id)[NSNull null]; +} + ++ (NSDictionary *)_rectDict:(CGRect)r { + return @{ @"x": [self _num:r.origin.x], + @"y": [self _num:r.origin.y], + @"width": [self _num:r.size.width], + @"height": [self _num:r.size.height] }; +} + ++ (unsigned long)_oidForItem:(LookinDisplayItem *)item { + // Matches HierarchyIndex.oid(of:): prefer view, then layer, then window. + if (item.viewObject.oid) { return item.viewObject.oid; } + if (item.layerObject.oid) { return item.layerObject.oid; } + if (item.windowObject.oid) { return item.windowObject.oid; } + return 0; +} + ++ (NSString *)_classNameForItem:(LookinDisplayItem *)item { + NSString *(^head)(LookinObject *) = ^NSString *(LookinObject *obj) { + return obj.classChainList.firstObject; + }; + NSString *name = head(item.viewObject) ?: head(item.layerObject) ?: head(item.windowObject); + return name; +} + ++ (NSString *)_roleForClassName:(NSString *)className { + if (!className) { return nil; } + static NSDictionary *map; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + map = @{ @"UIButton": @"button", @"NSButton": @"button", + @"UILabel": @"label", @"NSTextField": @"label", + @"UIImageView": @"image", @"NSImageView": @"image", + @"UITextField": @"textInput", + @"UITextView": @"textArea", @"NSTextView": @"textArea", + @"UISwitch": @"switch", + @"UISlider": @"slider", + @"UIScrollView": @"scroll", @"NSScrollView": @"scroll", + @"UITableView": @"table", @"NSTableView": @"table", + @"UICollectionView": @"collection", @"NSCollectionView": @"collection", + @"UIStackView": @"stack", @"NSStackView": @"stack", + @"UIWindow": @"window", @"NSWindow": @"window" }; + }); + NSString *role = map[className]; + if (role) { return role; } + if ([className containsString:@"Button"]) { return @"button"; } + if ([className containsString:@"Label"]) { return @"label"; } + return nil; +} + ++ (NSString *)_breadcrumbForItem:(LookinDisplayItem *)item { + // Root -> self chain of class names joined by " ▸ ", mirroring HierarchyIndex.breadcrumb. + NSMutableArray *chain = [NSMutableArray array]; + LookinDisplayItem *cur = item; + while (cur) { + NSString *label = [self _classNameForItem:cur] ?: @"UnknownView"; + [chain insertObject:label atIndex:0]; + cur = cur.superItem; + } + return [chain componentsJoinedByString:@" ▸ "]; +} + +#pragma mark - Attribute extraction + ++ (NSString *)_extractStringAttributeForItem:(LookinDisplayItem *)item identifiers:(NSArray *)identifiers { + // Parity with JSONShape.extractAttribute: scan attributesGroupList, match identifier substring. + for (NSString *wanted in identifiers) { + for (LookinAttributesGroup *group in item.attributesGroupList) { + for (LookinAttributesSection *section in group.attrSections) { + for (LookinAttribute *attr in section.attributes) { + if ([attr.identifier containsString:wanted]) { + if ([attr.value isKindOfClass:[NSString class]] && [(NSString *)attr.value length]) { + return attr.value; + } + } + } + } + } + } + return nil; +} + ++ (NSArray *)_attributeGroupsArrayForItem:(LookinDisplayItem *)item { + NSMutableArray *groupsArray = [NSMutableArray array]; + for (LookinAttributesGroup *group in [item queryAllAttrGroupList]) { + NSMutableArray *sectionsArray = [NSMutableArray array]; + for (LookinAttributesSection *section in group.attrSections) { + NSMutableArray *attrsArray = [NSMutableArray array]; + for (LookinAttribute *attr in section.attributes) { + NSMutableDictionary *attrDict = [NSMutableDictionary dictionary]; + if (attr.identifier.length) { attrDict[@"id"] = attr.identifier; } + NSString *title = [self _titleForAttribute:attr]; + if (title.length) { attrDict[@"title"] = title; } + attrDict[@"value"] = [self _jsonValueForAttribute:attr] ?: [NSNull null]; + [attrsArray addObject:attrDict]; + } + if (attrsArray.count == 0) { continue; } + NSMutableDictionary *sectionDict = [NSMutableDictionary dictionary]; + NSString *secTitle = [self _titleForSection:section]; + if (secTitle.length) { sectionDict[@"title"] = secTitle; } + sectionDict[@"attributes"] = attrsArray; + [sectionsArray addObject:sectionDict]; + } + if (sectionsArray.count == 0) { continue; } + NSMutableDictionary *groupDict = [NSMutableDictionary dictionary]; + NSString *gTitle = [self _titleForGroup:group]; + if (gTitle.length) { groupDict[@"title"] = gTitle; } + groupDict[@"sections"] = sectionsArray; + [groupsArray addObject:groupDict]; + } + return groupsArray; +} + +#pragma mark - Titles + ++ (NSString *)_titleForGroup:(LookinAttributesGroup *)group { + NSString *title = [group queryDisplayTitle]; + return title; +} + ++ (NSString *)_titleForSection:(LookinAttributesSection *)section { + if (section.isUserCustom) { + LookinAttribute *attr = section.attributes.firstObject; + return attr.displayTitle; + } + return [LookinDashboardBlueprint sectionTitleWithSectionID:section.identifier]; +} + ++ (NSString *)_titleForAttribute:(LookinAttribute *)attr { + if (attr.isUserCustom) { + return attr.displayTitle; + } + NSString *title = [LookinDashboardBlueprint briefTitleWithAttrID:attr.identifier]; + if (!title.length) { + title = [LookinDashboardBlueprint fullTitleWithAttrID:attr.identifier]; + } + return title; +} + +#pragma mark - Value formatting + ++ (id)_jsonValueForAttribute:(LookinAttribute *)attr { + id value = attr.value; + if (!value) { return [NSNull null]; } + + switch (attr.attrType) { + case LookinAttrTypeBOOL: + return @([value boolValue]); + + case LookinAttrTypeCGRect: { + if ([value isKindOfClass:[NSValue class]]) { + CGRect r = [value rectValue]; + return [self _rectDict:r]; + } + break; + } + case LookinAttrTypeCGPoint: { + if ([value isKindOfClass:[NSValue class]]) { + CGPoint p = [value pointValue]; + return @{ @"x": [self _num:p.x], @"y": [self _num:p.y] }; + } + break; + } + case LookinAttrTypeCGSize: { + if ([value isKindOfClass:[NSValue class]]) { + CGSize s = [value sizeValue]; + return @{ @"width": [self _num:s.width], @"height": [self _num:s.height] }; + } + break; + } + case LookinAttrTypeCGVector: { + CGFloat b[4] = {0}; + if ([self _readDoubles:b count:2 fromValue:value]) { + return @{ @"dx": [self _num:b[0]], @"dy": [self _num:b[1]] }; + } + break; + } + case LookinAttrTypeUIEdgeInsets: { + CGFloat b[4] = {0}; + if ([self _readDoubles:b count:4 fromValue:value]) { + return @{ @"top": [self _num:b[0]], @"left": [self _num:b[1]], @"bottom": [self _num:b[2]], @"right": [self _num:b[3]] }; + } + break; + } + case LookinAttrTypeUIOffset: { + CGFloat b[4] = {0}; + if ([self _readDoubles:b count:2 fromValue:value]) { + return @{ @"horizontal": [self _num:b[0]], @"vertical": [self _num:b[1]] }; + } + break; + } + case LookinAttrTypeUIColor: { + if ([value isKindOfClass:[NSArray class]]) { + NSArray *rgba = value; + if (rgba.count == 4) { + return @{ @"r": rgba[0], @"g": rgba[1], @"b": rgba[2], @"a": rgba[3] }; + } + return [self _sanitizeJSONValue:rgba]; + } + break; + } + case LookinAttrTypeJson: { + if ([value isKindOfClass:[NSString class]]) { + NSData *data = [(NSString *)value dataUsingEncoding:NSUTF8StringEncoding]; + id parsed = data ? [NSJSONSerialization JSONObjectWithData:data options:0 error:nil] : nil; + if (parsed) { return parsed; } + return value; + } + return [self _sanitizeJSONValue:value]; + } + default: + break; + } + return [self _sanitizeJSONValue:value]; +} + +/// Convert any object into a JSON-safe representation. ++ (id)_sanitizeJSONValue:(id)value { + if (!value || value == [NSNull null]) { return [NSNull null]; } + if ([value isKindOfClass:[NSString class]]) { return value; } + if ([value isKindOfClass:[NSNumber class]]) { + double d = [(NSNumber *)value doubleValue]; + if (!isfinite(d)) { return [NSNull null]; } + return value; + } + if ([value isKindOfClass:[NSArray class]]) { + NSMutableArray *out = [NSMutableArray array]; + for (id v in (NSArray *)value) { [out addObject:[self _sanitizeJSONValue:v]]; } + return out; + } + if ([value isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary *out = [NSMutableDictionary dictionary]; + [(NSDictionary *)value enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + out[[key description]] = [self _sanitizeJSONValue:obj]; + }]; + return out; + } + if ([value isKindOfClass:[NSValue class]]) { + // Best-effort decode of common geometry structs by encoded type. + const char *type = [(NSValue *)value objCType]; + if (type) { + if (strcmp(type, @encode(CGRect)) == 0) { return [self _rectDict:[value rectValue]]; } + if (strcmp(type, @encode(CGPoint)) == 0) { CGPoint p = [value pointValue]; return @{ @"x": @(p.x), @"y": @(p.y) }; } + if (strcmp(type, @encode(CGSize)) == 0) { CGSize s = [value sizeValue]; return @{ @"width": @(s.width), @"height": @(s.height) }; } + } + } + return [value description]; +} + +/// Reads `count` (<=4) CGFloat fields out of an NSValue-wrapped struct. ++ (BOOL)_readDoubles:(CGFloat *)buffer count:(NSUInteger)count fromValue:(id)value { + if (![value isKindOfClass:[NSValue class]] || count == 0 || count > 4) { return NO; } + CGFloat tmp[4] = {0}; + @try { + [(NSValue *)value getValue:tmp]; + } @catch (__unused NSException *e) { + return NO; + } + for (NSUInteger i = 0; i < count; i++) { buffer[i] = tmp[i]; } + return YES; +} + ++ (NSString *)_readableStringForAttribute:(LookinAttribute *)attr { + id json = [self _jsonValueForAttribute:attr]; + return [self _readableStringFromJSONValue:json]; +} + ++ (NSString *)_readableStringFromJSONValue:(id)value { + if (!value || value == [NSNull null]) { return @"nil"; } + if ([value isKindOfClass:[NSString class]]) { return value; } + if ([value isKindOfClass:[NSNumber class]]) { + NSNumber *num = value; + if (strcmp(num.objCType, @encode(BOOL)) == 0 || strcmp(num.objCType, @encode(char)) == 0) { + // Heuristic: 0/1 chars are usually BOOL in this data model. + if ([num intValue] == 0 || [num intValue] == 1) { + return [num boolValue] ? @"true" : @"false"; + } + } + return num.stringValue; + } + if ([value isKindOfClass:[NSArray class]]) { + NSMutableArray *parts = [NSMutableArray array]; + for (id v in (NSArray *)value) { [parts addObject:[self _readableStringFromJSONValue:v]]; } + return [NSString stringWithFormat:@"[%@]", [parts componentsJoinedByString:@", "]]; + } + if ([value isKindOfClass:[NSDictionary class]]) { + NSMutableArray *parts = [NSMutableArray array]; + [(NSDictionary *)value enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [parts addObject:[NSString stringWithFormat:@"%@: %@", key, [self _readableStringFromJSONValue:obj]]]; + }]; + return [NSString stringWithFormat:@"{%@}", [parts componentsJoinedByString:@", "]]; + } + return [value description]; +} + +@end diff --git a/LookInside/Hierarchy/LKHierarchyView.m b/LookInside/Hierarchy/LKHierarchyView.m index ac19cfa..7f81aa7 100644 --- a/LookInside/Hierarchy/LKHierarchyView.m +++ b/LookInside/Hierarchy/LKHierarchyView.m @@ -20,6 +20,7 @@ #import "LKAppsManager.h" #import "LKInspectableApp.h" #import "LookinDisplayItem+LookinClient.h" +#import "LKMCPNodeExporter.h" static NSString * const kMenuBindKey_RowView = @"view"; static CGFloat const kRowHeight = 28; @@ -383,6 +384,32 @@ - (void)menuNeedsUpdate:(NSMenu *)menu { })]; }]; + // MCP 数据导出 / 属性复制 + if (!displayItem.isUserCustom) { + [menu addItem:[NSMenuItem separatorItem]]; + [menu addItem:({ + NSMenuItem *item = [NSMenuItem new]; + item.target = self; + item.action = @selector(_handleExportNodeAnalysisRecursively:); + item.title = NSLocalizedString(@"Export node analysis data (incl. children)…", nil); + item; + })]; + [menu addItem:({ + NSMenuItem *item = [NSMenuItem new]; + item.target = self; + item.action = @selector(_handleExportNodeAnalysisSingle:); + item.title = NSLocalizedString(@"Export this node's analysis data…", nil); + item; + })]; + [menu addItem:({ + NSMenuItem *item = [NSMenuItem new]; + item.target = self; + item.action = @selector(_handleCopyAttributesData:); + item.title = NSLocalizedString(@"Copy property data", nil); + item; + })]; + } + NSArray *backingLayerItems = [self.dataSource swiftUIBackingLayerItemsForItem:displayItem]; LookinDisplayItem *swiftUISourceItem = [self.dataSource swiftUISourceItemForLayerItem:displayItem]; if (backingLayerItems.count || swiftUISourceItem) { @@ -559,6 +586,78 @@ - (void)_handleExportScreenshot:(NSMenuItem *)menuItem { [LKExportManager exportScreenshotWithDisplayItem:view.displayItem]; } +- (void)_handleExportNodeAnalysisRecursively:(NSMenuItem *)menuItem { + LKHierarchyRowView *view = [menuItem.menu lookin_getBindObjectForKey:kMenuBindKey_RowView]; + [self _exportAnalysisDataForItem:view.displayItem recursive:YES]; +} + +- (void)_handleExportNodeAnalysisSingle:(NSMenuItem *)menuItem { + LKHierarchyRowView *view = [menuItem.menu lookin_getBindObjectForKey:kMenuBindKey_RowView]; + [self _exportAnalysisDataForItem:view.displayItem recursive:NO]; +} + +- (void)_exportAnalysisDataForItem:(LookinDisplayItem *)item recursive:(BOOL)recursive { + if (!item) { + NSBeep(); + return; + } + NSError *error = nil; + NSData *data = [LKMCPNodeExporter jsonDataForItem:item recursive:recursive error:&error]; + if (!data) { + NSAlert *alert = [[NSAlert alloc] init]; + alert.messageText = NSLocalizedString(@"Failed to export node analysis data.", nil); + alert.informativeText = error.localizedDescription ?: @""; + [alert addButtonWithTitle:NSLocalizedString(@"OK", nil)]; + [alert runModal]; + return; + } + + NSSavePanel *panel = [NSSavePanel savePanel]; + panel.nameFieldStringValue = [LKMCPNodeExporter suggestedFileNameForItem:item recursive:recursive]; + panel.allowedFileTypes = @[@"json"]; + panel.canCreateDirectories = YES; + panel.title = recursive ? NSLocalizedString(@"Export node analysis data (incl. children)…", nil) + : NSLocalizedString(@"Export this node's analysis data…", nil); + + NSWindow *hostWindow = self.window; + void (^completion)(NSModalResponse) = ^(NSModalResponse result) { + if (result != NSModalResponseOK || !panel.URL) { + return; + } + NSError *writeError = nil; + BOOL ok = [data writeToURL:panel.URL options:NSDataWritingAtomic error:&writeError]; + if (!ok) { + NSAlert *alert = [[NSAlert alloc] init]; + alert.messageText = NSLocalizedString(@"Failed to save file.", nil); + alert.informativeText = writeError.localizedDescription ?: @""; + [alert addButtonWithTitle:NSLocalizedString(@"OK", nil)]; + [alert runModal]; + } + }; + if (hostWindow) { + [panel beginSheetModalForWindow:hostWindow completionHandler:completion]; + } else { + completion([panel runModal]); + } +} + +- (void)_handleCopyAttributesData:(NSMenuItem *)menuItem { + LKHierarchyRowView *view = [menuItem.menu lookin_getBindObjectForKey:kMenuBindKey_RowView]; + LookinDisplayItem *item = view.displayItem; + if (!item) { + NSBeep(); + return; + } + NSString *text = [LKMCPNodeExporter readableAttributesTextForItem:item]; + if (!text.length) { + NSBeep(); + return; + } + NSPasteboard *paste = [NSPasteboard generalPasteboard]; + [paste clearContents]; + [paste writeObjects:@[text]]; +} + - (void)_handleCopyDisplayItemName:(NSMenuItem *)menuItem { NSString *stringToCopy = menuItem.representedObject; diff --git a/LookInside/Info.plist b/LookInside/Info.plist index 3643c00..603b86f 100644 --- a/LookInside/Info.plist +++ b/LookInside/Info.plist @@ -4,6 +4,8 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + LookInside 酷狗版 CFBundleDocumentTypes diff --git a/LookInside/InfoPlist.xcstrings b/LookInside/InfoPlist.xcstrings index 441a621..46da1e6 100644 --- a/LookInside/InfoPlist.xcstrings +++ b/LookInside/InfoPlist.xcstrings @@ -1,19 +1,31 @@ { "sourceLanguage" : "en", "strings" : { + "CFBundleDisplayName" : { + "comment" : "Bundle display name", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "LookInside 酷狗版" + } + } + } + }, "CFBundleName" : { "comment" : "Bundle name", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "LookInside" + "value" : "LookInside 酷狗版" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "LookInside" + "value" : "LookInside 酷狗版" } } } diff --git a/LookInside/Localizable.xcstrings b/LookInside/Localizable.xcstrings index 1a2abe0..dd53c65 100644 --- a/LookInside/Localizable.xcstrings +++ b/LookInside/Localizable.xcstrings @@ -1,6 +1,9 @@ { "sourceLanguage" : "en", "strings" : { + "" : { + + }, " / " : { "comment" : "Separator between imported/autosaved CSV sources", "localizations" : { @@ -212,6 +215,16 @@ } } }, + "All Pages" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "所有页面" + } + } + } + }, "Allow future guessed rows to use autosaved module CSVs." : { "localizations" : { "en" : { @@ -260,6 +273,22 @@ } } }, + "Approve LookInside Injector" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Approve LookInside Injector" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "批准 LookInside 注入器" + } + } + } + }, "Attach" : { "localizations" : { "en" : { @@ -357,7 +386,6 @@ } }, "Automatic update checks are disabled in this community build." : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -549,6 +577,22 @@ } } }, + "Check Again" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Check Again" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新检查" + } + } + } + }, "Checking for updates…" : { "localizations" : { "en" : { @@ -886,6 +930,22 @@ } } }, + "Copy property data" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copy property data" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制属性数据" + } + } + } + }, "Copy text \"%@\"" : { "localizations" : { "en" : { @@ -903,6 +963,7 @@ } }, "Current LookInside app is too old to open this document. Current LookInside app version is %@, but the document version is %@." : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1190,6 +1251,16 @@ } } }, + "Drawer" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "侧边栏抽屉" + } + } + } + }, "Empty download payload." : { "localizations" : { "en" : { @@ -1223,6 +1294,7 @@ } }, "Enable LookInside Injector" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1238,6 +1310,22 @@ } } }, + "Enable LookInside Injector?" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enable LookInside Injector?" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用 LookInside 注入器?" + } + } + } + }, "Enter a module name, comma-separated candidate words, and a maximum word count." : { "localizations" : { "en" : { @@ -1350,6 +1438,22 @@ } } }, + "Export node analysis data (incl. children)…" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Export node analysis data (incl. children)…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出节点分析数据(含子节点)…" + } + } + } + }, "Export screenshot…" : { "localizations" : { "en" : { @@ -1366,6 +1470,22 @@ } } }, + "Export this node's analysis data…" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Export this node's analysis data…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出当前节点分析数据…" + } + } + } + }, "Extracting…" : { "localizations" : { "en" : { @@ -1446,6 +1566,22 @@ } } }, + "Failed to export node analysis data." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to export node analysis data." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出节点分析数据失败。" + } + } + } + }, "Failed to extract LookInside Auth Server.\n%@" : { "localizations" : { "en" : { @@ -1479,6 +1615,7 @@ } }, "Failed to get target object in iOS app" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1559,6 +1696,7 @@ } }, "Failed to open the document." : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1590,6 +1728,22 @@ } } }, + "Failed to save file." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to save file." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存文件失败。" + } + } + } + }, "Failed to search related methods: " : { "localizations" : { "en" : { @@ -2648,7 +2802,6 @@ } }, "License Timeout" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2968,7 +3121,24 @@ } } }, + "LookInside needs to enable its privileged injector before it can attach to another process. macOS may require an administrator to approve this in System Settings." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "LookInside needs to enable its privileged injector before it can attach to another process. macOS may require an administrator to approve this in System Settings." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "LookInside 需要先启用特权注入器,才能附加到其他进程。macOS 可能会要求管理员在系统设置中批准此操作。" + } + } + } + }, "LookInside opened System Settings → Login Items & Extensions for you.\n\nFind “LookInside” in the list, turn it on, then come back and click Attach to Running App again." : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3064,6 +3234,39 @@ } } }, + "macOS is waiting for administrator approval before it can run the LookInside Injector. Open Login Items & Extensions, enable LookInside, then return here and check again." : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "macOS is waiting for administrator approval before it can run the LookInside Injector. Open Login Items & Extensions, enable LookInside, then return here and check again." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "macOS 正在等待管理员批准后才能运行 LookInside 注入器。请打开“登录项与扩展”,启用 LookInside,然后回到这里重新检查。" + } + } + } + }, + "macOS is waiting for administrator approval before it can run the LookInside Injector. Open Login Items & Extensions, enable LookInside, then return to LookInside and click Attach to Running App again." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "macOS is waiting for administrator approval before it can run the LookInside Injector. Open Login Items & Extensions, enable LookInside, then return to LookInside and click Attach to Running App again." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "macOS 正在等待管理员批准后才能运行 LookInside 注入器。请打开“登录项与扩展”,启用 LookInside,然后回到 LookInside 再点击“附加到运行中的 App”。" + } + } + } + }, "Matched from %@." : { "localizations" : { "en" : { @@ -3513,6 +3716,7 @@ } }, "Open Settings Again" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3528,6 +3732,22 @@ } } }, + "Open System Settings" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open System Settings" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开系统设置" + } + } + } + }, "Operation failed." : { "localizations" : { "en" : { @@ -3593,6 +3813,7 @@ } }, "Perhaps the related object was deallocated. You can reload LookInside to get newest data." : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3609,6 +3830,7 @@ } }, "Perhaps your iOS app is paused with breakpoint in Xcode, blocked by other tasks in main thread, or moved to background state." : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -4523,6 +4745,7 @@ } }, "The document was created by a LookInside app with too old version. Current LookInside app version is %@, but the document version is %@." : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -4619,6 +4842,26 @@ } } }, + "The LookInside Injector daemon is missing from this app.\nPlease reinstall LookInside, then try again." : { + + }, + "The LookInside Injector daemon is missing from this app.\\nPlease reinstall LookInside, then try again." : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The LookInside Injector daemon is missing from this app.\\nPlease reinstall LookInside, then try again." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此 App 中缺少 LookInside 注入器守护进程。\\n请重新安装 LookInside,然后再试一次。" + } + } + } + }, "The LookInside Injector daemon is not enabled (status %@)." : { "localizations" : { "en" : { @@ -4651,6 +4894,38 @@ } } }, + "The LookInside Injector daemon returned an unsupported status (%d)." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The LookInside Injector daemon returned an unsupported status (%d)." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "LookInside 注入器守护进程返回了不支持的状态(%d)。" + } + } + } + }, + "The LookInside Injector is bundled correctly, but macOS can only enable it from an installed app. Move LookInside to the Applications folder, relaunch it, then click Attach to Running App again." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The LookInside Injector is bundled correctly, but macOS can only enable it from an installed app. Move LookInside to the Applications folder, relaunch it, then click Attach to Running App again." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "LookInside 注入器已包含在 App 中,但 macOS 只能从已安装的 App 启用它。请将 LookInside 移到“应用程序”文件夹,重新启动后再点击“附加到运行中的 App”。" + } + } + } + }, "The method was invoked successfully and no value was returned." : { "localizations" : { "en" : { @@ -4716,6 +4991,7 @@ } }, "The operation failed due to disconnection with the iOS app." : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -4909,7 +5185,6 @@ } }, "Timeout for license challenge and verification requests, in seconds. Default: 5s." : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -5148,6 +5423,9 @@ } } } + }, + "Updates Disabled" : { + }, "Updating default library…" : { "localizations" : { @@ -5437,167 +5715,7 @@ } } } - }, - "Approve LookInside Injector" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Approve LookInside Injector" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "批准 LookInside 注入器" - } - } - } - }, - "Check Again" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Check Again" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "重新检查" - } - } - } - }, - "Enable LookInside Injector?" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enable LookInside Injector?" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "启用 LookInside 注入器?" - } - } - } - }, - "LookInside needs to enable its privileged injector before it can attach to another process. macOS may require an administrator to approve this in System Settings." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "LookInside needs to enable its privileged injector before it can attach to another process. macOS may require an administrator to approve this in System Settings." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "LookInside 需要先启用特权注入器,才能附加到其他进程。macOS 可能会要求管理员在系统设置中批准此操作。" - } - } - } - }, - "Open System Settings" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Open System Settings" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "打开系统设置" - } - } - } - }, - "The LookInside Injector daemon is missing from this app.\\nPlease reinstall LookInside, then try again." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The LookInside Injector daemon is missing from this app.\\nPlease reinstall LookInside, then try again." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "此 App 中缺少 LookInside 注入器守护进程。\\n请重新安装 LookInside,然后再试一次。" - } - } - } - }, - "The LookInside Injector is bundled correctly, but macOS can only enable it from an installed app. Move LookInside to the Applications folder, relaunch it, then click Attach to Running App again." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The LookInside Injector is bundled correctly, but macOS can only enable it from an installed app. Move LookInside to the Applications folder, relaunch it, then click Attach to Running App again." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "LookInside 注入器已包含在 App 中,但 macOS 只能从已安装的 App 启用它。请将 LookInside 移到“应用程序”文件夹,重新启动后再点击“附加到运行中的 App”。" - } - } - } - }, - "The LookInside Injector daemon returned an unsupported status (%d)." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The LookInside Injector daemon returned an unsupported status (%d)." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "LookInside 注入器守护进程返回了不支持的状态(%d)。" - } - } - } - }, - "macOS is waiting for administrator approval before it can run the LookInside Injector. Open Login Items & Extensions, enable LookInside, then return here and check again." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "macOS is waiting for administrator approval before it can run the LookInside Injector. Open Login Items & Extensions, enable LookInside, then return here and check again." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "macOS 正在等待管理员批准后才能运行 LookInside 注入器。请打开“登录项与扩展”,启用 LookInside,然后回到这里重新检查。" - } - } - } - }, - "macOS is waiting for administrator approval before it can run the LookInside Injector. Open Login Items & Extensions, enable LookInside, then return to LookInside and click Attach to Running App again." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "macOS is waiting for administrator approval before it can run the LookInside Injector. Open Login Items & Extensions, enable LookInside, then return to LookInside and click Attach to Running App again." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "macOS 正在等待管理员批准后才能运行 LookInside 注入器。请打开“登录项与扩展”,启用 LookInside,然后回到 LookInside 再点击“附加到运行中的 App”。" - } - } - } } }, "version" : "1.1" -} +} \ No newline at end of file diff --git a/LookInside/Manager/LKAppMenuManager.m b/LookInside/Manager/LKAppMenuManager.m index 97112bb..a1578a0 100644 --- a/LookInside/Manager/LKAppMenuManager.m +++ b/LookInside/Manager/LKAppMenuManager.m @@ -24,6 +24,8 @@ static NSUInteger const kTag_SwiftUISupportCustomerSupport = 18; static NSUInteger const kTag_SwiftUISupportSubmenu = 19; +static NSString *const kLookInsideKugouEditionName = @"LookInside 酷狗版"; + static NSString *const kSwiftUISupportPurchaseURL = @"https://lookinside-app.com/purchase"; static NSString *const kSwiftUISupportCustomerSupportURL = @"mailto:support@lookinside-app.com"; @@ -62,6 +64,7 @@ @interface LKAppMenuManager () @property(nonatomic, copy) NSDictionary *delegatingTagToSelMap; @property(nonatomic, strong) NSMenu *recentDocumentsMenu; @property(nonatomic, strong) SPUStandardUpdaterController *updaterController; +@property(nonatomic, assign, getter=isUpdaterAvailable) BOOL updaterAvailable; @end @@ -81,15 +84,21 @@ + (id)allocWithZone:(struct _NSZone *)zone{ } - (NSString *)_applicationName { - NSString *bundleDisplayName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]; - if (bundleDisplayName.length > 0) { - return bundleDisplayName; + return kLookInsideKugouEditionName; +} + +- (BOOL)_hasValidSparkleConfiguration { + NSBundle *bundle = NSBundle.mainBundle; + NSString *feedURL = [bundle objectForInfoDictionaryKey:@"SUFeedURL"]; + NSString *publicKey = [bundle objectForInfoDictionaryKey:@"SUPublicEDKey"]; + + if (feedURL.length == 0 || publicKey.length == 0) { + return NO; } - NSString *bundleName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"]; - if (bundleName.length > 0) { - return bundleName; + if ([publicKey containsString:@"$("]) { + return NO; } - return [NSProcessInfo processInfo].processName ?: @"LookInside"; + return YES; } - (NSMenu *)_buildApplicationMenu { @@ -293,7 +302,10 @@ - (void)_installMainMenu { } - (void)setup { - self.updaterController = [[SPUStandardUpdaterController alloc] initWithStartingUpdater:YES updaterDelegate:nil userDriverDelegate:nil]; + self.updaterAvailable = [self _hasValidSparkleConfiguration]; + if (self.isUpdaterAvailable) { + self.updaterController = [[SPUStandardUpdaterController alloc] initWithStartingUpdater:YES updaterDelegate:nil userDriverDelegate:nil]; + } [self _installMainMenu]; self.delegatingTagToSelMap = @{ @@ -479,6 +491,15 @@ - (void)_handleExpansion:(NSMenuItem *)item { } - (void)_handleCheckUpdates { + if (!self.isUpdaterAvailable) { + NSAlert *alert = [[NSAlert alloc] init]; + alert.messageText = NSLocalizedString(@"Updates Disabled", nil); + alert.informativeText = NSLocalizedString(@"Automatic update checks are disabled in this community build.", nil); + alert.alertStyle = NSAlertStyleInformational; + [alert addButtonWithTitle:NSLocalizedString(@"OK", nil)]; + [alert runModal]; + return; + } [self.updaterController checkForUpdates:nil]; } diff --git a/LookInside/Manager/LKPreferenceManager.h b/LookInside/Manager/LKPreferenceManager.h index f5701a8..2dd7b19 100644 --- a/LookInside/Manager/LKPreferenceManager.h +++ b/LookInside/Manager/LKPreferenceManager.h @@ -64,6 +64,12 @@ typedef NS_ENUM(NSInteger, LookinMeasureState) { @property(nonatomic, strong, readonly) LookinBOOLMsgAttribute *showHiddenItems; +/// 酷狗(KuGou)专用:是否在层级里展开 KGMainViewController 的全部页面(关闭折叠逻辑)。默认 NO。 +@property(nonatomic, strong, readonly) LookinBOOLMsgAttribute *kgShowAllPages; + +/// 酷狗(KuGou)专用:是否显示 KGMainViewController 的抽屉(_setViewContainer)。默认 NO。 +@property(nonatomic, strong, readonly) LookinBOOLMsgAttribute *kgShowDrawer; + // 范围是 0 ~ 1 @property(nonatomic, strong, readonly) LookinDoubleMsgAttribute *zInterspace; diff --git a/LookInside/Manager/LKPreferenceManager.m b/LookInside/Manager/LKPreferenceManager.m index 0b3dcdd..3cdb29a 100644 --- a/LookInside/Manager/LKPreferenceManager.m +++ b/LookInside/Manager/LKPreferenceManager.m @@ -22,6 +22,8 @@ static NSString * const Key_PreviousClientVersion = @"preVer"; static NSString * const Key_ShowOutline = @"showOutline"; static NSString * const Key_ShowHiddenItems = @"showHiddenItems"; +static NSString * const Key_KGShowAllPages = @"KGShouldDisableLookinHook"; +static NSString * const Key_KGShowDrawer = @"KGShouldShowSetContainer"; static NSString * const Key_RgbaFormat = @"egbaFormat"; static NSString * const Key_ZInterspace = @"zInterspace_v095"; static NSString * const Key_AppearanceType = @"appearanceType"; @@ -93,7 +95,17 @@ - (instancetype)init { [userDefaults setObject:@(NO) forKey:Key_ShowHiddenItems]; } [self.showHiddenItems subscribe:self action:@selector(_handleShowHiddenItemsChange:) relatedObject:nil]; - + + // 酷狗(KuGou):是否显示全部页面(关闭折叠),默认 NO + NSNumber *obj_kgShowAllPages = [userDefaults objectForKey:Key_KGShowAllPages]; + _kgShowAllPages = [LookinBOOLMsgAttribute attributeWithBOOL:(obj_kgShowAllPages != nil ? obj_kgShowAllPages.boolValue : NO)]; + [self.kgShowAllPages subscribe:self action:@selector(_handleKGShowAllPagesDidChange:) relatedObject:nil]; + + // 酷狗(KuGou):是否显示抽屉(_setViewContainer),默认 NO + NSNumber *obj_kgShowDrawer = [userDefaults objectForKey:Key_KGShowDrawer]; + _kgShowDrawer = [LookinBOOLMsgAttribute attributeWithBOOL:(obj_kgShowDrawer != nil ? obj_kgShowDrawer.boolValue : NO)]; + [self.kgShowDrawer subscribe:self action:@selector(_handleKGShowDrawerDidChange:) relatedObject:nil]; + NSNumber *obj_doubleClickBehavior = [userDefaults objectForKey:Key_DoubleClickBehavior]; if (obj_doubleClickBehavior) { _doubleClickBehavior = [obj_doubleClickBehavior intValue]; @@ -355,6 +367,18 @@ - (void)_handleShowHiddenItemsChange:(LookinMsgActionParams *)param { } } +- (void)_handleKGShowAllPagesDidChange:(LookinMsgActionParams *)param { + if (self.shouldStoreToLocal) { + [[NSUserDefaults standardUserDefaults] setObject:@(param.boolValue) forKey:Key_KGShowAllPages]; + } +} + +- (void)_handleKGShowDrawerDidChange:(LookinMsgActionParams *)param { + if (self.shouldStoreToLocal) { + [[NSUserDefaults standardUserDefaults] setObject:@(param.boolValue) forKey:Key_KGShowDrawer]; + } +} + - (void)setRgbaFormat:(BOOL)rgbaFormat { if (_rgbaFormat == rgbaFormat) { return; diff --git a/LookInside/Static/LKStaticWindowController.m b/LookInside/Static/LKStaticWindowController.m index 6b3718e..df128e4 100644 --- a/LookInside/Static/LKStaticWindowController.m +++ b/LookInside/Static/LKStaticWindowController.m @@ -294,7 +294,7 @@ - (void)popupAllInspectableAppsWithSource:(MenuPopoverAppsListControllerEventSou } - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar { - NSMutableArray *ret = @[LKToolBarIdentifier_Reload, LKToolBarIdentifier_FastMode, LKToolBarIdentifier_App, LKToolBarIdentifier_SwiftUIMode, NSToolbarFlexibleSpaceItemIdentifier, LKToolBarIdentifier_Dimension, LKToolBarIdentifier_Rotation, LKToolBarIdentifier_Setting, NSToolbarFlexibleSpaceItemIdentifier, LKToolBarIdentifier_Scale, NSToolbarFlexibleSpaceItemIdentifier, LKToolBarIdentifier_Measure, LKToolBarIdentifier_Console].mutableCopy; + NSMutableArray *ret = @[LKToolBarIdentifier_Reload, LKToolBarIdentifier_FastMode, LKToolBarIdentifier_KGShowAllPages, LKToolBarIdentifier_KGShowDrawer, LKToolBarIdentifier_App, LKToolBarIdentifier_SwiftUIMode, NSToolbarFlexibleSpaceItemIdentifier, LKToolBarIdentifier_Dimension, LKToolBarIdentifier_Rotation, LKToolBarIdentifier_Setting, NSToolbarFlexibleSpaceItemIdentifier, LKToolBarIdentifier_Scale, NSToolbarFlexibleSpaceItemIdentifier, LKToolBarIdentifier_Measure, LKToolBarIdentifier_Console].mutableCopy; if ([[[LKMessageManager sharedInstance] queryMessages] count] > 0) { [ret addObject:LKToolBarIdentifier_Message]; } @@ -346,6 +346,12 @@ - (nullable NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:( } else if ([item.itemIdentifier isEqualToString:LKToolBarIdentifier_FastMode]) { item.target = self; item.action = @selector(handleFastMode); + } else if ([item.itemIdentifier isEqualToString:LKToolBarIdentifier_KGShowAllPages]) { + item.target = self; + item.action = @selector(_handleKGShowAllPages); + } else if ([item.itemIdentifier isEqualToString:LKToolBarIdentifier_KGShowDrawer]) { + item.target = self; + item.action = @selector(_handleKGShowDrawer); } } return item; @@ -471,6 +477,20 @@ - (void)_handleFreeRotation { [[LKPreferenceManager mainManager].freeRotation setBOOLValue:!boolValue ignoreSubscriber:nil]; } +- (void)_handleKGShowAllPages { + BOOL boolValue = [LKPreferenceManager mainManager].kgShowAllPages.currentBOOLValue; + [[LKPreferenceManager mainManager].kgShowAllPages setBOOLValue:!boolValue ignoreSubscriber:nil]; + // 折叠逻辑会就地修改层级树,需要重新从设备拉取一份干净的层级后再按新开关折叠 + [self _handleReload]; +} + +- (void)_handleKGShowDrawer { + BOOL boolValue = [LKPreferenceManager mainManager].kgShowDrawer.currentBOOLValue; + [[LKPreferenceManager mainManager].kgShowDrawer setBOOLValue:!boolValue ignoreSubscriber:nil]; + // 折叠逻辑会就地修改层级树,需要重新从设备拉取一份干净的层级后再按新开关折叠 + [self _handleReload]; +} + #pragma mark - - (void)appMenuManagerDidSelectReload { diff --git a/LookInside/Toolbar/LKWindowToolbarHelper.h b/LookInside/Toolbar/LKWindowToolbarHelper.h index bd86bf6..d7e39b0 100644 --- a/LookInside/Toolbar/LKWindowToolbarHelper.h +++ b/LookInside/Toolbar/LKWindowToolbarHelper.h @@ -22,6 +22,10 @@ extern NSToolbarItemIdentifier const LKToolBarIdentifier_Measure; extern NSToolbarItemIdentifier const LKToolBarIdentifier_Message; extern NSToolbarItemIdentifier const LKToolBarIdentifier_FastMode; extern NSToolbarItemIdentifier const LKToolBarIdentifier_SwiftUIMode; +/// 酷狗(KuGou):显示全部页面(关闭 KGMainViewController 折叠) +extern NSToolbarItemIdentifier const LKToolBarIdentifier_KGShowAllPages; +/// 酷狗(KuGou):显示抽屉(_setViewContainer) +extern NSToolbarItemIdentifier const LKToolBarIdentifier_KGShowDrawer; @class LKPreferenceManager, LookinAppInfo; diff --git a/LookInside/Toolbar/LKWindowToolbarHelper.m b/LookInside/Toolbar/LKWindowToolbarHelper.m index 16e7ec7..0985e3d 100644 --- a/LookInside/Toolbar/LKWindowToolbarHelper.m +++ b/LookInside/Toolbar/LKWindowToolbarHelper.m @@ -29,6 +29,8 @@ NSToolbarItemIdentifier const LKToolBarIdentifier_Message = @"18"; NSToolbarItemIdentifier const LKToolBarIdentifier_FastMode = @"19"; NSToolbarItemIdentifier const LKToolBarIdentifier_SwiftUIMode = @"20"; +NSToolbarItemIdentifier const LKToolBarIdentifier_KGShowAllPages = @"21"; +NSToolbarItemIdentifier const LKToolBarIdentifier_KGShowDrawer = @"22"; static NSString * const Key_BindingPreferenceManager = @"PreferenceManager"; @@ -232,7 +234,41 @@ - (NSToolbarItem *)makeToolBarItemWithIdentifier:(NSToolbarItemIdentifier)identi [manager.fastMode subscribe:self action:@selector(_handleFastModeDidChange:) relatedObject:button sendAtOnce:YES]; return item; } - + + if ([identifier isEqualToString:LKToolBarIdentifier_KGShowAllPages]) { + NSImage *image = [NSImage imageWithSystemSymbolName:@"rectangle.stack" accessibilityDescription:nil]; + image.template = YES; + + NSButton *button = [NSButton new]; + [button setImage:image]; + button.bezelStyle = NSBezelStyleTexturedRounded; + [button setButtonType:NSButtonTypePushOnPushOff]; + + NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:LKToolBarIdentifier_KGShowAllPages]; + item.label = NSLocalizedString(@"All Pages", nil); + item.view = button; + + [manager.kgShowAllPages subscribe:self action:@selector(_handleKGShowAllPagesDidChange:) relatedObject:button sendAtOnce:YES]; + return item; + } + + if ([identifier isEqualToString:LKToolBarIdentifier_KGShowDrawer]) { + NSImage *image = [NSImage imageWithSystemSymbolName:@"sidebar.left" accessibilityDescription:nil]; + image.template = YES; + + NSButton *button = [NSButton new]; + [button setImage:image]; + button.bezelStyle = NSBezelStyleTexturedRounded; + [button setButtonType:NSButtonTypePushOnPushOff]; + + NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:LKToolBarIdentifier_KGShowDrawer]; + item.label = NSLocalizedString(@"Drawer", nil); + item.view = button; + + [manager.kgShowDrawer subscribe:self action:@selector(_handleKGShowDrawerDidChange:) relatedObject:button sendAtOnce:YES]; + return item; + } + if ([identifier isEqualToString:LKToolBarIdentifier_Add]) { NSImage *image = [NSImage imageNamed:NSImageNameAddTemplate]; image.template = YES; @@ -325,6 +361,16 @@ - (void)_handleFastModeDidChange:(LookinMsgActionParams *)param { button.state = boolValue ? NSControlStateValueOn : NSControlStateValueOff; } +- (void)_handleKGShowAllPagesDidChange:(LookinMsgActionParams *)param { + NSButton *button = param.relatedObject; + button.state = param.boolValue ? NSControlStateValueOn : NSControlStateValueOff; +} + +- (void)_handleKGShowDrawerDidChange:(LookinMsgActionParams *)param { + NSButton *button = param.relatedObject; + button.state = param.boolValue ? NSControlStateValueOn : NSControlStateValueOff; +} + - (void)_handleDimensionDidChange:(LookinMsgActionParams *)param { LookinPreviewDimension newDimension = param.integerValue; NSSegmentedControl *control = param.relatedObject; diff --git a/LookinCore.podspec b/LookinCore.podspec new file mode 100644 index 0000000..f8f0888 --- /dev/null +++ b/LookinCore.podspec @@ -0,0 +1,33 @@ +require_relative 'LookinPodspecHelpers' + +Pod::Spec.new do |s| + s.name = 'LookinCore' + LookinPodspecHelpers.apply_common_metadata(s, 'Shared LookInside data models and utilities.') + + s.module_name = 'LookinCore' + s.static_framework = true + s.dependency 'LookinServerBase' + + s.source_files = [ + 'Sources/LookinCore/**/*.{h,m}', + 'Sources/LookinServer/Server/Category/UIColor+LookinServer.h', + 'Sources/LookinServer/Server/Category/UIImage+LookinServer.h' + ] + s.exclude_files = 'Sources/LookinCore/include/LookinCore.h' + s.public_header_files = 'Sources/LookinCore/**/*.h' + s.private_header_files = [ + 'Sources/LookinServer/Server/Category/UIColor+LookinServer.h', + 'Sources/LookinServer/Server/Category/UIImage+LookinServer.h' + ] + + s.pod_target_xcconfig = { + 'GCC_PREPROCESSOR_DEFINITIONS' => LookinPodspecHelpers.base_defines, + 'HEADER_SEARCH_PATHS' => LookinPodspecHelpers.header_search_paths( + 'Sources/LookinCore', + 'Sources/LookinCore/include', + 'Sources/LookinCore/Category', + 'Sources/LookinCore/Peertalk', + 'Sources/LookinServer/Server/Category' + ) + } +end diff --git a/LookinPodspecHelpers.rb b/LookinPodspecHelpers.rb new file mode 100644 index 0000000..074542d --- /dev/null +++ b/LookinPodspecHelpers.rb @@ -0,0 +1,29 @@ +module LookinPodspecHelpers + VERSION = '0.1.0' + HOMEPAGE = 'https://lookinside-app.com' + AUTHOR = { 'LookInside' => 'support@lookinside-app.com' }.freeze + + module_function + + def apply_common_metadata(spec, summary) + spec.version = VERSION + spec.summary = summary + spec.description = summary + spec.homepage = HOMEPAGE + spec.license = { :type => 'MIT', :file => 'LICENSE' } + spec.author = AUTHOR + spec.source = { :path => '.' } + spec.ios.deployment_target = '12.0' + spec.tvos.deployment_target = '12.0' + spec.osx.deployment_target = '11.0' + spec.requires_arc = true + end + + def base_defines + '$(inherited) SHOULD_COMPILE_LOOKIN_SERVER=1' + end + + def header_search_paths(*paths) + paths.flatten.map { |path| %("$(PODS_TARGET_SRCROOT)/#{path}") }.unshift('$(inherited)').join(' ') + end +end diff --git a/LookinServer.podspec b/LookinServer.podspec new file mode 100644 index 0000000..772041f --- /dev/null +++ b/LookinServer.podspec @@ -0,0 +1,75 @@ +require_relative 'LookinPodspecHelpers' + +Pod::Spec.new do |s| + s.name = 'LookinServer' + LookinPodspecHelpers.apply_common_metadata(s, 'LookInside debug server for iOS applications.') + + s.module_name = 'LookinServer' + s.requires_arc = true + s.static_framework = true + s.default_subspec = 'Server' + + s.subspec 'Base' do |ss| + ss.source_files = 'Sources/LookinServerBase/**/*.{h,m}' + ss.public_header_files = 'Sources/LookinServerBase/**/*.h' + ss.pod_target_xcconfig = { + 'GCC_PREPROCESSOR_DEFINITIONS' => LookinPodspecHelpers.base_defines, + 'HEADER_SEARCH_PATHS' => LookinPodspecHelpers.header_search_paths('Sources/LookinServerBase') + } + end + + s.subspec 'Core' do |ss| + ss.dependency 'LookinServer/Base' + ss.source_files = [ + 'Sources/LookinCore/**/*.{h,m}', + 'Sources/LookinServer/Server/Category/UIColor+LookinServer.h', + 'Sources/LookinServer/Server/Category/UIImage+LookinServer.h' + ] + ss.exclude_files = 'Sources/LookinCore/include/LookinCore.h' + ss.public_header_files = 'Sources/LookinCore/**/*.h' + ss.private_header_files = [ + 'Sources/LookinServer/Server/Category/UIColor+LookinServer.h', + 'Sources/LookinServer/Server/Category/UIImage+LookinServer.h' + ] + ss.pod_target_xcconfig = { + 'GCC_PREPROCESSOR_DEFINITIONS' => LookinPodspecHelpers.base_defines, + 'HEADER_SEARCH_PATHS' => LookinPodspecHelpers.header_search_paths( + 'Sources/LookinCore', + 'Sources/LookinCore/include', + 'Sources/LookinCore/Category', + 'Sources/LookinCore/Peertalk', + 'Sources/LookinServer/Server/Category' + ) + } + end + + s.subspec 'Shared' do |ss| + ss.dependency 'LookinServer/Core' + ss.dependency 'LookinServer/Base' + end + + s.subspec 'Server' do |ss| + ss.dependency 'LookinServer/Shared' + ss.source_files = [ + 'Sources/LookinServer/Server/**/*.{h,m}' + ] + ss.public_header_files = [ + 'Sources/LookinServer/Server/LookinServer.h', + 'Sources/LookinServer/include/LookinServer.h', + 'Sources/LookinServer/Server/**/*.h' + ] + ss.tvos.exclude_files = [ + 'Sources/LookinServer/Server/Category/UIWindowScene+LookinServer.{h,m}' + ] + ss.pod_target_xcconfig = { + 'GCC_PREPROCESSOR_DEFINITIONS' => LookinPodspecHelpers.base_defines, + 'HEADER_SEARCH_PATHS' => LookinPodspecHelpers.header_search_paths( + 'Sources/LookinServer/Server', + 'Sources/LookinServer/Server/Category', + 'Sources/LookinServer/Server/Connection', + 'Sources/LookinServer/Server/Connection/RequestHandler', + 'Sources/LookinServer/Server/Others' + ) + } + end +end diff --git a/LookinServerBase.podspec b/LookinServerBase.podspec new file mode 100644 index 0000000..f44a2f4 --- /dev/null +++ b/LookinServerBase.podspec @@ -0,0 +1,17 @@ +require_relative 'LookinPodspecHelpers' + +Pod::Spec.new do |s| + s.name = 'LookinServerBase' + LookinPodspecHelpers.apply_common_metadata(s, 'Base model support for LookInside server libraries.') + + s.module_name = 'LookinServerBase' + s.static_framework = true + + s.source_files = 'Sources/LookinServerBase/**/*.{h,m}' + s.public_header_files = 'Sources/LookinServerBase/**/*.h' + + s.pod_target_xcconfig = { + 'GCC_PREPROCESSOR_DEFINITIONS' => LookinPodspecHelpers.base_defines, + 'HEADER_SEARCH_PATHS' => LookinPodspecHelpers.header_search_paths('Sources/LookinServerBase') + } +end diff --git a/LookinServerDynamic.podspec b/LookinServerDynamic.podspec new file mode 100644 index 0000000..326fd7d --- /dev/null +++ b/LookinServerDynamic.podspec @@ -0,0 +1,34 @@ +require_relative 'LookinPodspecHelpers' + +Pod::Spec.new do |s| + s.name = 'LookinServerDynamic' + LookinPodspecHelpers.apply_common_metadata(s, 'Dynamic-framework CocoaPods package for LookInside debug server.') + + s.module_name = 'LookinServer' + s.static_framework = false + s.dependency 'LookinServer/Shared' + + s.source_files = [ + 'Sources/LookinServer/Server/**/*.{h,m}' + ] + + s.public_header_files = [ + 'Sources/LookinServer/Server/LookinServer.h', + 'Sources/LookinServer/include/LookinServer.h', + 'Sources/LookinServer/Server/**/*.h' + ] + s.tvos.exclude_files = [ + 'Sources/LookinServer/Server/Category/UIWindowScene+LookinServer.{h,m}' + ] + + s.pod_target_xcconfig = { + 'GCC_PREPROCESSOR_DEFINITIONS' => LookinPodspecHelpers.base_defines, + 'HEADER_SEARCH_PATHS' => LookinPodspecHelpers.header_search_paths( + 'Sources/LookinServer/Server', + 'Sources/LookinServer/Server/Category', + 'Sources/LookinServer/Server/Connection', + 'Sources/LookinServer/Server/Connection/RequestHandler', + 'Sources/LookinServer/Server/Others' + ) + } +end diff --git a/LookinServerInjected.podspec b/LookinServerInjected.podspec new file mode 100644 index 0000000..c985ead --- /dev/null +++ b/LookinServerInjected.podspec @@ -0,0 +1,16 @@ +require_relative 'LookinPodspecHelpers' + +Pod::Spec.new do |s| + s.name = 'LookinServerInjected' + LookinPodspecHelpers.apply_common_metadata(s, 'Constructor-based LookInside server bootstrap for injected builds.') + + s.module_name = 'LookinServerInjected' + s.static_framework = false + s.dependency 'LookinServerDynamic' + + s.source_files = 'Sources/LookinServerInjected/**/*.{h,m}' + + s.pod_target_xcconfig = { + 'GCC_PREPROCESSOR_DEFINITIONS' => LookinPodspecHelpers.base_defines + } +end diff --git a/LookinShared.podspec b/LookinShared.podspec new file mode 100644 index 0000000..49ec9f5 --- /dev/null +++ b/LookinShared.podspec @@ -0,0 +1,11 @@ +require_relative 'LookinPodspecHelpers' + +Pod::Spec.new do |s| + s.name = 'LookinShared' + LookinPodspecHelpers.apply_common_metadata(s, 'Aggregate CocoaPods dependency for LookInside shared libraries.') + + s.module_name = 'LookinShared' + s.static_framework = true + s.dependency 'LookinCore' + s.dependency 'LookinServerBase' +end diff --git a/Package.swift b/Package.swift index 507c7fe..96c102f 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( platforms: [ .iOS(.v12), .tvOS(.v12), - .macOS(.v11), + .macOS(.v13), ], products: [ .library( @@ -42,8 +42,18 @@ let package = Package( type: .dynamic, targets: ["LookinServerInjected"] ), + .library( + name: "LookinMCPCore", + targets: ["LookinMCPCore"] + ), + .executable( + name: "lookinside-mcp", + targets: ["LookinMCPServer"] + ), + ], + dependencies: [ + .package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", from: "0.11.0"), ], - dependencies: [], targets: [ .target( name: "LookinServerBase", @@ -65,6 +75,7 @@ let package = Package( .headerSearchPath("."), .headerSearchPath("Category"), .headerSearchPath("Peertalk"), + .headerSearchPath("../LookinServer/Server/Category"), ], cxxSettings: sharedCXXDefines ), @@ -109,5 +120,36 @@ let package = Package( .linkedFramework("UIKit", .when(platforms: [.iOS, .tvOS])), ] ), + .target( + name: "LookinMCPCore", + dependencies: ["LookinCore"], + path: "Sources/LookinMCPCore", + swiftSettings: [ + .define("SHOULD_COMPILE_LOOKIN_SERVER"), + .define("SPM_LOOKIN_SERVER_ENABLED"), + ] + ), + .executableTarget( + name: "LookinMCPServer", + dependencies: [ + "LookinMCPCore", + .product(name: "MCP", package: "swift-sdk"), + ], + path: "Sources/LookinMCPServer", + swiftSettings: [ + .define("SHOULD_COMPILE_LOOKIN_SERVER"), + .define("SPM_LOOKIN_SERVER_ENABLED"), + ] + ), + .testTarget( + name: "LookinMCPCoreTests", + dependencies: ["LookinMCPCore"], + path: "Tests/LookinMCPCoreTests", + resources: [.process("Fixtures")], + swiftSettings: [ + .define("SHOULD_COMPILE_LOOKIN_SERVER"), + .define("SPM_LOOKIN_SERVER_ENABLED"), + ] + ), ] ) diff --git a/README.md b/README.md index 56eb6e4..1bde6d9 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,20 @@ LookInside continues the work of [Lookin](https://lookin.work/), the original iO Use [LookInside-Release](https://github.com/LookInsideApp/LookInside-Release) with Swift Package Manager or CocoaPods. +## MCP integration (Debug) + +LookInside ships an optional MCP server, `lookinside-mcp`, so AI coding agents (Claude Desktop, Claude Code, Cursor, Windsurf, VS Code, …) can inspect the running Debug build directly — hierarchy, screenshots, element search, highlight, layout/accessibility diagnostics, and a one-shot bug report. + +Build and try it: + +```sh +./Scripts/build-mcp-server.sh +./build/lookinside-mcp health +./build/lookinside-mcp print-config claude-desktop +``` + +See [docs/mcp.md](docs/mcp.md) for the full feature set, [docs/mcp-client-configs.md](docs/mcp-client-configs.md) for client setup, and [docs/mcp-troubleshooting.md](docs/mcp-troubleshooting.md) if something looks off. + ## License GPL-3.0. See [`LICENSE`](LICENSE). diff --git a/Scripts/build-dmg.sh b/Scripts/build-dmg.sh new file mode 100755 index 0000000..0aaeb34 --- /dev/null +++ b/Scripts/build-dmg.sh @@ -0,0 +1,620 @@ +#!/usr/bin/env bash +# +# Build a notarized LookInside DMG. +# +# Usage: +# Scripts/build-dmg.sh [--keychain-profile PROFILE] [--no-notarize] +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +WORKSPACE_FILE="$PROJECT_ROOT/LookInside.xcworkspace" +SCHEME="LookInside" +CONFIGURATION="Release" +BUNDLE_IDENTIFIER="cn.vanjay.lookinside" +DEVELOPMENT_TEAM="X6B6C6U6QV" +DEVELOPER_ID_APPLICATION_REQUIREMENT="anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.13] exists" +KEYCHAIN_PROFILE="vanjay_mac_stapler" +SKIP_NOTARIZE=false +BUILD_ROOT="$PROJECT_ROOT/build" +DERIVED_DATA_PATH="${DERIVED_DATA_PATH:-$BUILD_ROOT/DerivedData}" +SOURCE_PACKAGES_PATH="${SOURCE_PACKAGES_PATH:-$DERIVED_DATA_PATH/SourcePackages}" +ARCHIVE_PATH="$BUILD_ROOT/dmg-archive/LookInside.xcarchive" +DMG_OUTPUT_DIR="$BUILD_ROOT/dmg" +DMG_WORK_DIR="$BUILD_ROOT/dmg-tmp" +PACKAGE_SCM_PROVIDER="${PACKAGE_SCM_PROVIDER:-system}" +PACKAGE_AUTH_PROVIDER="${PACKAGE_AUTH_PROVIDER:-}" +SIGNING_IDENTITY="${SIGNING_IDENTITY:-}" +APP_PRODUCT_NAME="" +APP_EXECUTABLE_NAME="" +MARKETING_VERSION="" +CURRENT_PROJECT_VERSION="" +APP_PATH="" +INJECTOR_REPO="$PROJECT_ROOT/../LookInside-Injector" + +usage() { + cat <<'EOF' +Usage: Scripts/build-dmg.sh [options] + +Options: + --keychain-profile Notarytool keychain profile. Default: vanjay_mac_stapler. + --no-notarize Build the DMG without submitting to Apple notarization. + --help, -h Show this help. +EOF +} + +log() { + echo "==> $*" +} + +fail() { + echo "Error: $*" >&2 + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || fail "Missing required command: $1" +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --keychain-profile) + [[ -n "${2:-}" && "$2" != --* ]] || fail "--keychain-profile requires a profile name." + KEYCHAIN_PROFILE="$2" + shift 2 + ;; + --no-notarize) + SKIP_NOTARIZE=true + shift + ;; + --help | -h) + usage + exit 0 + ;; + *) + fail "Unknown option: $1" + ;; + esac + done +} + +format_output() { + if command -v xcbeautify >/dev/null 2>&1; then + xcbeautify --disable-logging + else + cat + fi +} + +read_build_setting() { + local key="$1" + local overrides=( + PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_IDENTIFIER" + DEVELOPMENT_TEAM="$DEVELOPMENT_TEAM" + ) + + xcodebuild \ + -workspace "$WORKSPACE_FILE" \ + -scheme "$SCHEME" \ + -configuration "$CONFIGURATION" \ + -showBuildSettings \ + "${overrides[@]}" 2>/dev/null | + awk -F' = ' -v search_key="$key" '$1 ~ search_key"$" { print $2; exit }' +} + +load_build_settings() { + APP_PRODUCT_NAME="$(read_build_setting FULL_PRODUCT_NAME)" + APP_EXECUTABLE_NAME="$(read_build_setting EXECUTABLE_NAME)" + MARKETING_VERSION="$(read_build_setting MARKETING_VERSION)" + CURRENT_PROJECT_VERSION="$(read_build_setting CURRENT_PROJECT_VERSION)" + + [[ -n "$APP_PRODUCT_NAME" ]] || fail "Unable to read FULL_PRODUCT_NAME." + [[ -n "$APP_EXECUTABLE_NAME" ]] || fail "Unable to read EXECUTABLE_NAME." + [[ -n "$MARKETING_VERSION" ]] || fail "Unable to read MARKETING_VERSION." + [[ -n "$CURRENT_PROJECT_VERSION" ]] || fail "Unable to read CURRENT_PROJECT_VERSION." + + APP_PATH="$ARCHIVE_PATH/Products/Applications/$APP_PRODUCT_NAME" +} + +note_release_prerequisites() { + if [[ ! -d "$INJECTOR_REPO" ]]; then + log "LookInside-Injector repo not found; building DMG without the injector daemon." + log "Injector-dependent attach/injection features will be unavailable in this build." + fi +} + +detect_signing_identity() { + if [[ -n "$SIGNING_IDENTITY" ]]; then + return + fi + + SIGNING_IDENTITY="$( + security find-identity -v -p codesigning 2>/dev/null | + grep "Developer ID Application: .*($DEVELOPMENT_TEAM)" | + head -n 1 | + awk '{print $2}' + )" + + [[ -n "$SIGNING_IDENTITY" ]] || fail "No Developer ID Application identity found for team $DEVELOPMENT_TEAM." +} + +is_mach_o_file() { + local path="$1" + file -b "$path" 2>/dev/null | grep -q "Mach-O" +} + +path_contains_symlink() { + local path="$1" + local root="${2:-}" + local current="$path" + + if [[ -n "$root" ]]; then + while [[ "$current" == "$root" || "$current" == "$root/"* ]]; do + [[ -L "$current" ]] && return 0 + [[ "$current" == "$root" ]] && break + current="$(dirname "$current")" + done + return 1 + fi + + while [[ "$current" != "/" && "$current" != "." ]]; do + [[ -L "$current" ]] && return 0 + current="$(dirname "$current")" + done + + return 1 +} + +should_skip_nested_code_path() { + local path="$1" + local root="${2:-}" + + [[ "$path" == *"/Versions/Current"* ]] && return 0 + path_contains_symlink "$path" "$root" +} + +sign_code_path() { + local path="$1" + + log "Signing nested code: ${path#$PROJECT_ROOT/}" + codesign \ + --sign "$SIGNING_IDENTITY" \ + --options runtime \ + --timestamp \ + --verbose=4 \ + --force \ + "$path" +} + +remove_legacy_code_resources() { + local legacy_code_resources="$APP_PATH/Contents/CodeResources" + + if [[ -e "$legacy_code_resources" || -L "$legacy_code_resources" ]]; then + rm -f "$legacy_code_resources" + fi +} + +sign_nested_code() { + local main_executable="$APP_PATH/Contents/MacOS/$APP_EXECUTABLE_NAME" + local mach_o_files=() + local bundles=() + local candidate + + while IFS= read -r candidate; do + [[ "$candidate" == "$main_executable" ]] && continue + should_skip_nested_code_path "$candidate" "$APP_PATH/Contents" && continue + if is_mach_o_file "$candidate"; then + mach_o_files+=("$candidate") + fi + done < <(find "$APP_PATH/Contents" -type f -print) + + while IFS= read -r candidate; do + bundles+=("$candidate") + done < <( + find "$APP_PATH/Contents" -type d \ + \( -name "*.app" -o -name "*.appex" -o -name "*.bundle" -o -name "*.framework" -o -name "*.xpc" \) \ + -print | + awk '{ print length, $0 }' | + sort -rn | + cut -d' ' -f2- + ) + + for candidate in "${mach_o_files[@]}"; do + sign_code_path "$candidate" + done + + for candidate in "${bundles[@]}"; do + should_skip_nested_code_path "$candidate" "$APP_PATH/Contents" && continue + sign_code_path "$candidate" + done +} + +verify_developer_id_signature() { + local path="$1" + + codesign \ + --verify \ + --strict \ + --verbose=2 \ + -R="$DEVELOPER_ID_APPLICATION_REQUIREMENT" \ + "$path" +} + +verify_developer_id_signatures() { + local candidate + local signed_paths_file + local legacy_code_resources="$APP_PATH/Contents/CodeResources" + + [[ ! -e "$legacy_code_resources" && ! -L "$legacy_code_resources" ]] || + fail "Legacy code signature resource envelope is present: $legacy_code_resources" + + signed_paths_file="$(mktemp "${TMPDIR:-/tmp}/lookinside-dmg-signed-code.XXXXXX")" + printf "%s\n" "$APP_PATH" >>"$signed_paths_file" + + while IFS= read -r candidate; do + should_skip_nested_code_path "$candidate" "$APP_PATH/Contents" && continue + printf "%s\n" "$candidate" >>"$signed_paths_file" + done < <( + find "$APP_PATH/Contents" -type d \ + \( -name "*.app" -o -name "*.appex" -o -name "*.bundle" -o -name "*.framework" -o -name "*.xpc" \) \ + -print + ) + + while IFS= read -r candidate; do + should_skip_nested_code_path "$candidate" "$APP_PATH/Contents" && continue + if is_mach_o_file "$candidate"; then + printf "%s\n" "$candidate" >>"$signed_paths_file" + fi + done < <(find "$APP_PATH/Contents" -type f -print) + + while IFS= read -r candidate; do + log "Verifying Developer ID signature: ${candidate#$PROJECT_ROOT/}" + verify_developer_id_signature "$candidate" + done < <(sort -u "$signed_paths_file") + + rm -f "$signed_paths_file" +} + +build_app_unsigned() { + local app_products_dir="$DERIVED_DATA_PATH/Build/Products/$CONFIGURATION" + local built_app_path="$app_products_dir/$APP_PRODUCT_NAME" + local resolve_args=( + -skipMacroValidation + -skipPackagePluginValidation + -skipPackageUpdates + -disablePackageRepositoryCache + -skipPackageSignatureValidation + -packageFingerprintPolicy warn + -packageSigningEntityPolicy warn + -scmProvider "$PACKAGE_SCM_PROVIDER" + -workspace "$WORKSPACE_FILE" + -scheme "$SCHEME" + -configuration "$CONFIGURATION" + -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_PATH" + ) + local xcodebuild_args=( + -skipMacroValidation + -skipPackagePluginValidation + -disableAutomaticPackageResolution + -onlyUsePackageVersionsFromResolvedFile + -skipPackageUpdates + -disablePackageRepositoryCache + -skipPackageSignatureValidation + -packageFingerprintPolicy warn + -packageSigningEntityPolicy warn + -scmProvider "$PACKAGE_SCM_PROVIDER" + -workspace "$WORKSPACE_FILE" + -scheme "$SCHEME" + -configuration "$CONFIGURATION" + -destination "generic/platform=macOS" + -derivedDataPath "$DERIVED_DATA_PATH" + -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_PATH" + PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_IDENTIFIER" + DEVELOPMENT_TEAM="$DEVELOPMENT_TEAM" + LOOKINSIDE_ALLOW_MISSING_INJECTOR=YES + CODE_SIGNING_ALLOWED=NO + ) + + if [[ -n "${SPARKLE_PUBLIC_ED_KEY:-}" ]]; then + xcodebuild_args+=(SPARKLE_PUBLIC_ED_KEY="$SPARKLE_PUBLIC_ED_KEY") + fi + + if [[ -n "$PACKAGE_AUTH_PROVIDER" ]]; then + resolve_args+=(-packageAuthorizationProvider "$PACKAGE_AUTH_PROVIDER") + xcodebuild_args+=(-packageAuthorizationProvider "$PACKAGE_AUTH_PROVIDER") + fi + + rm -rf "$ARCHIVE_PATH" "$DERIVED_DATA_PATH/Build" "$DERIVED_DATA_PATH/Logs" + mkdir -p "$DERIVED_DATA_PATH" "$SOURCE_PACKAGES_PATH" + + log "Syncing derived source mirror" + bash "$PROJECT_ROOT/Scripts/sync-derived-source.sh" + + log "Resolving Swift package dependencies" + xcodebuild "${resolve_args[@]}" -resolvePackageDependencies 2>&1 | format_output + + log "Building $APP_PRODUCT_NAME without Xcode signing" + xcodebuild "${xcodebuild_args[@]}" clean build 2>&1 | format_output + + [[ -d "$built_app_path" ]] || fail "Built app not found at $built_app_path" + + log "Assembling archive at ${ARCHIVE_PATH#$PROJECT_ROOT/}" + mkdir -p "$ARCHIVE_PATH/Products/Applications" + ditto "$built_app_path" "$APP_PATH" + /usr/libexec/PlistBuddy -c "Add :ApplicationProperties dict" "$ARCHIVE_PATH/Info.plist" >/dev/null + /usr/libexec/PlistBuddy -c "Add :ApplicationProperties:ApplicationPath string Applications/$APP_PRODUCT_NAME" "$ARCHIVE_PATH/Info.plist" >/dev/null + /usr/libexec/PlistBuddy -c "Add :ApplicationProperties:CFBundleIdentifier string $BUNDLE_IDENTIFIER" "$ARCHIVE_PATH/Info.plist" >/dev/null + /usr/libexec/PlistBuddy -c "Add :ApplicationProperties:CFBundleShortVersionString string $MARKETING_VERSION" "$ARCHIVE_PATH/Info.plist" >/dev/null + /usr/libexec/PlistBuddy -c "Add :ApplicationProperties:CFBundleVersion string $CURRENT_PROJECT_VERSION" "$ARCHIVE_PATH/Info.plist" >/dev/null + /usr/libexec/PlistBuddy -c "Add :ArchiveVersion integer 2" "$ARCHIVE_PATH/Info.plist" >/dev/null + /usr/libexec/PlistBuddy -c "Add :Name string LookInside" "$ARCHIVE_PATH/Info.plist" >/dev/null + /usr/libexec/PlistBuddy -c "Add :SchemeName string $SCHEME" "$ARCHIVE_PATH/Info.plist" >/dev/null +} + +sign_app_bundle() { + local entitlements_path="$PROJECT_ROOT/LookInside/Lookin.entitlements" + local main_executable="$APP_PATH/Contents/MacOS/$APP_EXECUTABLE_NAME" + + [[ -f "$main_executable" ]] || fail "Main app executable not found at $main_executable" + chmod 755 "$main_executable" + [[ -x "$main_executable" ]] || fail "Main app executable is not executable: $main_executable" + + remove_legacy_code_resources + sign_nested_code + + log "Signing app bundle" + codesign \ + --sign "$SIGNING_IDENTITY" \ + --entitlements "$entitlements_path" \ + --options runtime \ + --timestamp \ + --verbose=4 \ + --force \ + "$APP_PATH" + + log "Verifying app signature" + codesign --verify --deep --strict --verbose=2 "$APP_PATH" + verify_developer_id_signatures +} + +verify_bundle_identifier() { + local actual_bundle_identifier + + actual_bundle_identifier="$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "$APP_PATH/Contents/Info.plist")" + [[ "$actual_bundle_identifier" == "$BUNDLE_IDENTIFIER" ]] || + fail "CFBundleIdentifier is $actual_bundle_identifier, expected $BUNDLE_IDENTIFIER" +} + +generate_dmg_background() { + local output_path="$1" + + /usr/bin/swift - "$output_path" <<'SWIFT' +import Cocoa +import Foundation + +guard CommandLine.arguments.count >= 2 else { + fputs("Error: Missing output path\n", stderr) + exit(1) +} + +let outputPath = CommandLine.arguments[1] + +final class BackgroundView: NSView { + private enum C { + static let width: CGFloat = 620 + static let height: CGFloat = 360 + static let background = NSColor(srgbRed: 0.94, green: 0.96, blue: 0.98, alpha: 1) + static let header = NSColor(srgbRed: 0.74, green: 0.86, blue: 0.93, alpha: 1) + static let accent = NSColor(srgbRed: 0.08, green: 0.38, blue: 0.56, alpha: 1) + static let text = NSColor(srgbRed: 0.09, green: 0.14, blue: 0.18, alpha: 1) + static let subtext = NSColor(srgbRed: 0.28, green: 0.35, blue: 0.41, alpha: 1) + static let panelFill = NSColor.white.withAlphaComponent(0.95) + static let panelStroke = NSColor(srgbRed: 0.70, green: 0.80, blue: 0.87, alpha: 1) + static let titleFont = NSFont(name: "Avenir Next Demi Bold", size: 26) ?? .systemFont(ofSize: 26, weight: .semibold) + static let subtitleFont = NSFont(name: "Avenir Next Regular", size: 14) ?? .systemFont(ofSize: 14) + } + + override func draw(_ dirtyRect: NSRect) { + let bg = NSBezierPath(roundedRect: bounds, xRadius: 24, yRadius: 24) + C.background.setFill() + bg.fill() + + NSGradient(starting: C.header, ending: C.background)?.draw(in: NSRect(x: 0, y: 248, width: bounds.width, height: 112), angle: -90) + drawText("Drag LookInside to Applications", font: C.titleFont, color: C.text, in: NSRect(x: 56, y: 286, width: 508, height: 34)) + drawText("Install the macOS UI inspector by dragging it onto the Applications shortcut", font: C.subtitleFont, color: C.subtext, in: NSRect(x: 50, y: 242, width: 520, height: 24)) + drawPanel(NSRect(x: 58, y: 72, width: 192, height: 168)) + drawPanel(NSRect(x: 370, y: 72, width: 192, height: 168)) + + let arrowBody = NSBezierPath() + arrowBody.move(to: NSPoint(x: 254, y: 156)) + arrowBody.line(to: NSPoint(x: 338, y: 156)) + C.accent.setStroke() + arrowBody.lineWidth = 14 + arrowBody.stroke() + + let arrowHead = NSBezierPath() + arrowHead.move(to: NSPoint(x: 326, y: 178)) + arrowHead.line(to: NSPoint(x: 364, y: 156)) + arrowHead.line(to: NSPoint(x: 326, y: 134)) + arrowHead.close() + C.accent.setFill() + arrowHead.fill() + } + + private func drawPanel(_ rect: NSRect) { + let panel = NSBezierPath(roundedRect: rect, xRadius: 26, yRadius: 26) + C.panelFill.setFill() + panel.fill() + C.panelStroke.setStroke() + panel.lineWidth = 2 + panel.stroke() + } + + private func drawText(_ text: String, font: NSFont, color: NSColor, in rect: NSRect) { + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .center + let attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: color, .paragraphStyle: paragraph] + (text as NSString).draw(in: rect, withAttributes: attrs) + } +} + +let frame = NSRect(x: 0, y: 0, width: 620, height: 360) +let view = BackgroundView(frame: frame) +guard let rep = view.bitmapImageRepForCachingDisplay(in: frame) else { + fputs("Error: Could not create bitmap rep\n", stderr) + exit(1) +} +view.cacheDisplay(in: frame, to: rep) +guard let data = rep.representation(using: .png, properties: [:]) else { + fputs("Error: Could not generate PNG data\n", stderr) + exit(1) +} +try data.write(to: URL(fileURLWithPath: outputPath)) +SWIFT +} + +create_pretty_dmg() { + local dmg_path="$1" + local volume_name="$2" + local staging_dir="$DMG_WORK_DIR/staging" + local background_dir="$staging_dir/.background" + local background_path="$background_dir/installer-background.png" + local rw_dmg_path="$DMG_WORK_DIR/${volume_name}.temp.dmg" + local device + local attach_output + local mounted_volume_path + local mounted_volume_name + + rm -rf "$DMG_WORK_DIR" + mkdir -p "$staging_dir" "$background_dir" + ditto "$APP_PATH" "$staging_dir/$APP_PRODUCT_NAME" + ln -s /Applications "$staging_dir/Applications" + generate_dmg_background "$background_path" + chflags hidden "$background_dir" 2>/dev/null || true + + hdiutil create -volname "$volume_name" \ + -srcfolder "$staging_dir" \ + -fs HFS+ \ + -fsargs "-c c=64,a=16,e=16" \ + -ov -format UDRW \ + "$rw_dmg_path" + + attach_output="$(hdiutil attach -readwrite -noverify -noautoopen "$rw_dmg_path")" + device="$(printf '%s\n' "$attach_output" | awk -F '\t' '/\/Volumes\// {print $1; exit}')" + mounted_volume_path="$(printf '%s\n' "$attach_output" | awk -F '\t' '/\/Volumes\// {print $NF; exit}')" + mounted_volume_name="$(basename "$mounted_volume_path")" + + if [[ -z "$device" || -z "$mounted_volume_name" ]]; then + echo "$attach_output" >&2 + fail "Unable to mount temporary DMG." + fi + + osascript <