From 66ef61bc31dfa8ed36c397255e4b3ca7c064a6f3 Mon Sep 17 00:00:00 2001 From: Aryamirsepasi Date: Fri, 3 Jan 2025 12:26:30 +0100 Subject: [PATCH 1/9] better chat window --- .../Commands/WritingOption.swift | 16 +++++------ macOS/writing-tools/UI/AboutView.swift | 2 +- macOS/writing-tools/UI/PopupView.swift | 4 +-- macOS/writing-tools/UI/ResponseView.swift | 27 +++++++++++-------- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/macOS/writing-tools/Commands/WritingOption.swift b/macOS/writing-tools/Commands/WritingOption.swift index 48c52df..6945d27 100644 --- a/macOS/writing-tools/Commands/WritingOption.swift +++ b/macOS/writing-tools/Commands/WritingOption.swift @@ -14,35 +14,35 @@ enum WritingOption: String, CaseIterable, Identifiable { switch self { case .proofread: return """ - You are a grammar proofreading assistant. Output ONLY the corrected text without any additional comments. Maintain the original text structure and writing style. Respond in the same language as the input (e.g., English US, French). Do not answer or respond to the user's text content. If the text is absolutely incompatible with this (e.g., totally random gibberish), output "ERROR_TEXT_INCOMPATIBLE_WITH_REQUEST". + You are a grammar proofreading assistant. Output ONLY the corrected text without any additional comments. Maintain the original text structure and writing style. Always respond in the same language as the input text. If the text is completely incompatible (e.g., totally random gibberish), output "ERROR_TEXT_INCOMPATIBLE_WITH_REQUEST". """ case .rewrite: return """ - You are a writing assistant. Rewrite the text provided by the user to improve phrasing. Output ONLY the rewritten text without additional comments. Respond in the same language as the input (e.g., English US, French). Do not answer or respond to the user's text content. If the text is absolutely incompatible with proofreading (e.g., totally random gibberish), output "ERROR_TEXT_INCOMPATIBLE_WITH_REQUEST". + You are a writing assistant. Rewrite the text provided by the user to improve phrasing. Output ONLY the rewritten text without additional comments. Always respond in the same language as the input text. If the text is completely incompatible (e.g., totally random gibberish), output "ERROR_TEXT_INCOMPATIBLE_WITH_REQUEST". """ case .friendly: return """ - You are a writing assistant. Rewrite the text provided by the user to be more friendly. Output ONLY the friendly text without additional comments. Respond in the same language as the input (e.g., English US, French). Do not answer or respond to the user's text content. If the text is absolutely incompatible with rewriting (e.g., totally random gibberish), output "ERROR_TEXT_INCOMPATIBLE_WITH_REQUEST". + You are a writing assistant. Rewrite the text provided by the user to make it more friendly. Output ONLY the friendly version without additional comments. Always respond in the same language as the input text. If the text is completely incompatible (e.g., totally random gibberish), output "ERROR_TEXT_INCOMPATIBLE_WITH_REQUEST". """ case .professional: return """ - You are a writing assistant. Rewrite the text provided by the user to sound more professional. Output ONLY the professional text without additional comments. Respond in the same language as the input (e.g., English US, French). Do not answer or respond to the user's text content. If the text is absolutely incompatible with this (e.g., totally random gibberish), output "ERROR_TEXT_INCOMPATIBLE_WITH_REQUEST". + You are a writing assistant. Rewrite the text provided by the user to make it sound more professional. Output ONLY the professional version without additional comments. Always respond in the same language as the input text. If the text is completely incompatible (e.g., totally random gibberish), output "ERROR_TEXT_INCOMPATIBLE_WITH_REQUEST". """ case .concise: return """ - You are a writing assistant. Rewrite the text provided by the user to be slightly more concise in tone, thus making it just a bit shorter. Do not change the text too much or be too reductive. Output ONLY the concise version without additional comments. Respond in the same language as the input (e.g., English US, French). Do not answer or respond to the user's text content. If the text is absolutely incompatible with this (e.g., totally random gibberish), output "ERROR_TEXT_INCOMPATIBLE_WITH_REQUEST". + You are a writing assistant. Rewrite the text provided by the user to make it slightly more concise, shortening it without losing key information. Output ONLY the concise version without additional comments. Always respond in the same language as the input text. If the text is completely incompatible (e.g., totally random gibberish), output "ERROR_TEXT_INCOMPATIBLE_WITH_REQUEST". """ case .summary: return """ - You are a summarisation assistant. Provide a succinct summary of the text provided by the user. The summary should be succinct yet encompass all the key insightful points. To make it quite legible and readable, you MUST use Markdown formatting (bold, italics, underline...). You should add line spacing between your paragraphs/lines. Only if appropriate, you could also use headings (only the very small ones), lists, tables, etc. Don't be repetitive or too verbose. Output ONLY the summary without additional comments. Respond in the same language as the input (e.g., English US, French). Do not answer or respond to the user's text content. If the text is absolutely incompatible with summarisation (e.g., totally random gibberish), output "ERROR_TEXT_INCOMPATIBLE_WITH_REQUEST". + You are a summarization assistant. Provide a succinct summary of the text provided by the user. The summary should be concise, capturing all key points and using Markdown formatting (bold, italics, headings, etc.) for better readability. Always respond in the same language as the input text. If the text is completely incompatible (e.g., totally random gibberish), output "ERROR_TEXT_INCOMPATIBLE_WITH_REQUEST". """ case .keyPoints: return """ - You are an assistant that extracts key points from text provided by the user. Output ONLY the key points without additional comments. You MUST use Markdown formatting (lists, bold, italics, underline, etc. as appropriate) to make it quite legible and readable. Don't be repetitive or too verbose. Respond in the same language as the input (e.g., English US, French). Do not answer or respond to the user's text content. If the text is absolutely incompatible with extracting key points (e.g., totally random gibberish), output "ERROR_TEXT_INCOMPATIBLE_WITH_REQUEST". + You are an assistant that extracts key points from text provided by the user. Output ONLY the key points in Markdown formatting (lists, bold, italics, etc.). Always respond in the same language as the input text. If the text is completely incompatible (e.g., totally random gibberish), output "ERROR_TEXT_INCOMPATIBLE_WITH_REQUEST". """ case .table: return """ - You are an assistant that converts text provided by the user into a Markdown table. Output ONLY the table without additional comments. Respond in the same language as the input (e.g., English US, French). Do not answer or respond to the user's text content. If the text is completely incompatible with this with conversion, output "ERROR_TEXT_INCOMPATIBLE_WITH_REQUEST". + You are an assistant that converts text provided by the user into a Markdown table. Output ONLY the table without additional comments. Always respond in the same language as the input text. If the text is completely incompatible (e.g., totally random gibberish), output "ERROR_TEXT_INCOMPATIBLE_WITH_REQUEST". """ } } diff --git a/macOS/writing-tools/UI/AboutView.swift b/macOS/writing-tools/UI/AboutView.swift index 7ef8cdb..82d24c7 100644 --- a/macOS/writing-tools/UI/AboutView.swift +++ b/macOS/writing-tools/UI/AboutView.swift @@ -36,7 +36,7 @@ struct AboutView: View { Divider() - Text("Version: Beta 5 (Based on Windows Port version 6.0)") + Text("Version: Beta 6 (Based on Windows Port version 6.0)") .font(.caption) Button("Check for Updates") { diff --git a/macOS/writing-tools/UI/PopupView.swift b/macOS/writing-tools/UI/PopupView.swift index d5aae42..968f25f 100644 --- a/macOS/writing-tools/UI/PopupView.swift +++ b/macOS/writing-tools/UI/PopupView.swift @@ -196,8 +196,8 @@ struct PopupView: View { do { let systemPrompt = """ You are a writing and coding assistant. Your sole task is to respond to the user's instruction thoughtfully and comprehensively. - If the instruction is a question, provide a detailed answer. - If it's a request for help, provide clear guidance and examples where appropriate. + If the instruction is a question, provide a detailed answer. But always return the best and most accurate answer and not different options. + If it's a request for help, provide clear guidance and examples where appropriate. Make sure tu use the language used or specified by the user instruction. Use Markdown formatting to make your response more readable. """ diff --git a/macOS/writing-tools/UI/ResponseView.swift b/macOS/writing-tools/UI/ResponseView.swift index e805066..116c271 100644 --- a/macOS/writing-tools/UI/ResponseView.swift +++ b/macOS/writing-tools/UI/ResponseView.swift @@ -70,6 +70,7 @@ struct ResponseView: View { ForEach(viewModel.messages) { message in ChatMessageView(message: message, fontSize: viewModel.fontSize) .id(message.id) + .frame(maxWidth: .infinity, alignment: message.role == "user" ? .trailing : .leading) } } .padding() @@ -118,6 +119,7 @@ struct ResponseView: View { struct ChatMessageView: View { let message: ChatMessage let fontSize: CGFloat + @Environment(\.colorScheme) var colorScheme var body: some View { HStack(alignment: .top) { @@ -126,13 +128,15 @@ struct ChatMessageView: View { } VStack(alignment: message.role == "user" ? .trailing : .leading, spacing: 4) { - Markdown(message.content) - .font(.system(size: fontSize)) - .textSelection(.enabled) - .padding() - .frame(maxWidth: 280, alignment: .leading) // Always left-align the text - .background(message.role == "user" ? Color.accentColor.opacity(0.1) : Color(.controlBackgroundColor)) - .cornerRadius(12) + ViewThatFits(in: .horizontal) { + Markdown(message.content) + .font(.system(size: fontSize)) + .textSelection(.enabled) + .padding() + .frame(minWidth: 100, idealWidth: 450, maxWidth: 600, alignment: .leading) + .background(message.role == "user" ? Color.accentColor.opacity(0.1) : Color(.controlBackgroundColor)) + .cornerRadius(12) + } Text(message.timestamp.formatted(.dateTime.hour().minute())) .font(.caption2) @@ -146,6 +150,7 @@ struct ChatMessageView: View { } } + extension View { func maxWidth(_ width: CGFloat) -> some View { frame(maxWidth: width) @@ -200,10 +205,10 @@ final class ResponseViewModel: ObservableObject { let result = try await AppState.shared.activeProvider.processText( systemPrompt: """ - You are a helpful AI assistant continuing a conversation. You have access to the entire conversation history and should maintain context when responding. - Provide clear and direct responses, maintaining the same format and style as your previous responses. - If appropriate, use Markdown formatting to make your response more readable. - Consider all previous messages when formulating your response. + You are a writing and coding assistant. Your sole task is to respond to the user's instruction thoughtfully and comprehensively. + If the instruction is a question, provide a detailed answer. But always return the best and most accurate answer and not different options. + If it's a request for help, provide clear guidance and examples where appropriate. Make sure tu use the language used or specified by the user instruction. + Use Markdown formatting to make your response more readable. """, userPrompt: contextualPrompt ) From 8f68994e16ab80621bb2265960b219722cca1f6d Mon Sep 17 00:00:00 2001 From: Aryamirsepasi Date: Sat, 4 Jan 2025 00:07:36 +0100 Subject: [PATCH 2/9] added pbxproj file for xcode cloning --- macOS/.gitignore | 2 +- macOS/writing-tools.xcodeproj/project.pbxproj | 616 ++++++++++++++++++ 2 files changed, 617 insertions(+), 1 deletion(-) create mode 100644 macOS/writing-tools.xcodeproj/project.pbxproj diff --git a/macOS/.gitignore b/macOS/.gitignore index b6668bb..a4fe0dd 100644 --- a/macOS/.gitignore +++ b/macOS/.gitignore @@ -48,8 +48,8 @@ fastlane/test_output # Personal settings *.plist -*.xcodeproj *.xcodeproj/project.xcworkspace *.xcodeproj/xcuserdata/ +*.xcodeproj/xcshareddata/ *.xcodeproj/xcuserdata/**/*.xcuserdatad *.xcodeproj/xcuserdata/**/*.xccheckout \ No newline at end of file diff --git a/macOS/writing-tools.xcodeproj/project.pbxproj b/macOS/writing-tools.xcodeproj/project.pbxproj new file mode 100644 index 0000000..fde02d5 --- /dev/null +++ b/macOS/writing-tools.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 2A57862B2CEFB15900DB01FC /* HotKey in Frameworks */ = {isa = PBXBuildFile; productRef = 2A57862A2CEFB15900DB01FC /* HotKey */; }; + 2AA154122CF2636900D27D18 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 2AA154112CF2636900D27D18 /* MarkdownUI */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 2ABCBC302CDEB607001E4B5E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2ABCBC162CDEB606001E4B5E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2ABCBC1D2CDEB606001E4B5E; + remoteInfo = "swift-writing-tools"; + }; + 2ABCBC3A2CDEB607001E4B5E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2ABCBC162CDEB606001E4B5E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2ABCBC1D2CDEB606001E4B5E; + remoteInfo = "swift-writing-tools"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 2ABCBC1E2CDEB606001E4B5E /* writing-tools.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "writing-tools.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2ABCBC2F2CDEB607001E4B5E /* writing-toolsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "writing-toolsTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2ABCBC392CDEB607001E4B5E /* writing-toolsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "writing-toolsUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 2A71341F2CF9D78E005ECCA8 /* Exceptions for "writing-tools" folder in "writing-tools" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 2ABCBC1D2CDEB606001E4B5E /* writing-tools */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 2ABCBC202CDEB606001E4B5E /* writing-tools */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 2A71341F2CF9D78E005ECCA8 /* Exceptions for "writing-tools" folder in "writing-tools" target */, + ); + path = "writing-tools"; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2ABCBC1B2CDEB606001E4B5E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A57862B2CEFB15900DB01FC /* HotKey in Frameworks */, + 2AA154122CF2636900D27D18 /* MarkdownUI in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2ABCBC2C2CDEB607001E4B5E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2ABCBC362CDEB607001E4B5E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2ABCBC152CDEB606001E4B5E = { + isa = PBXGroup; + children = ( + 2ABCBC202CDEB606001E4B5E /* writing-tools */, + 2ABCBC1F2CDEB606001E4B5E /* Products */, + ); + sourceTree = ""; + }; + 2ABCBC1F2CDEB606001E4B5E /* Products */ = { + isa = PBXGroup; + children = ( + 2ABCBC1E2CDEB606001E4B5E /* writing-tools.app */, + 2ABCBC2F2CDEB607001E4B5E /* writing-toolsTests.xctest */, + 2ABCBC392CDEB607001E4B5E /* writing-toolsUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 2ABCBC1D2CDEB606001E4B5E /* writing-tools */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2ABCBC432CDEB607001E4B5E /* Build configuration list for PBXNativeTarget "writing-tools" */; + buildPhases = ( + 2ABCBC1A2CDEB606001E4B5E /* Sources */, + 2ABCBC1B2CDEB606001E4B5E /* Frameworks */, + 2ABCBC1C2CDEB606001E4B5E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 2ABCBC202CDEB606001E4B5E /* writing-tools */, + ); + name = "writing-tools"; + packageProductDependencies = ( + 2A57862A2CEFB15900DB01FC /* HotKey */, + 2AA154112CF2636900D27D18 /* MarkdownUI */, + ); + productName = "swift-writing-tools"; + productReference = 2ABCBC1E2CDEB606001E4B5E /* writing-tools.app */; + productType = "com.apple.product-type.application"; + }; + 2ABCBC2E2CDEB607001E4B5E /* writing-toolsTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2ABCBC462CDEB607001E4B5E /* Build configuration list for PBXNativeTarget "writing-toolsTests" */; + buildPhases = ( + 2ABCBC2B2CDEB607001E4B5E /* Sources */, + 2ABCBC2C2CDEB607001E4B5E /* Frameworks */, + 2ABCBC2D2CDEB607001E4B5E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2ABCBC312CDEB607001E4B5E /* PBXTargetDependency */, + ); + name = "writing-toolsTests"; + packageProductDependencies = ( + ); + productName = "swift-writing-toolsTests"; + productReference = 2ABCBC2F2CDEB607001E4B5E /* writing-toolsTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 2ABCBC382CDEB607001E4B5E /* writing-toolsUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2ABCBC492CDEB607001E4B5E /* Build configuration list for PBXNativeTarget "writing-toolsUITests" */; + buildPhases = ( + 2ABCBC352CDEB607001E4B5E /* Sources */, + 2ABCBC362CDEB607001E4B5E /* Frameworks */, + 2ABCBC372CDEB607001E4B5E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2ABCBC3B2CDEB607001E4B5E /* PBXTargetDependency */, + ); + name = "writing-toolsUITests"; + packageProductDependencies = ( + ); + productName = "swift-writing-toolsUITests"; + productReference = 2ABCBC392CDEB607001E4B5E /* writing-toolsUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 2ABCBC162CDEB606001E4B5E /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1610; + LastUpgradeCheck = 1620; + TargetAttributes = { + 2ABCBC1D2CDEB606001E4B5E = { + CreatedOnToolsVersion = 16.1; + }; + 2ABCBC2E2CDEB607001E4B5E = { + CreatedOnToolsVersion = 16.1; + TestTargetID = 2ABCBC1D2CDEB606001E4B5E; + }; + 2ABCBC382CDEB607001E4B5E = { + CreatedOnToolsVersion = 16.1; + TestTargetID = 2ABCBC1D2CDEB606001E4B5E; + }; + }; + }; + buildConfigurationList = 2ABCBC192CDEB606001E4B5E /* Build configuration list for PBXProject "writing-tools" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2ABCBC152CDEB606001E4B5E; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 2A5786292CEFB15900DB01FC /* XCRemoteSwiftPackageReference "HotKey" */, + 2AA154102CF2636900D27D18 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 2ABCBC1F2CDEB606001E4B5E /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2ABCBC1D2CDEB606001E4B5E /* writing-tools */, + 2ABCBC2E2CDEB607001E4B5E /* writing-toolsTests */, + 2ABCBC382CDEB607001E4B5E /* writing-toolsUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 2ABCBC1C2CDEB606001E4B5E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2ABCBC2D2CDEB607001E4B5E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2ABCBC372CDEB607001E4B5E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 2ABCBC1A2CDEB606001E4B5E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2ABCBC2B2CDEB607001E4B5E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2ABCBC352CDEB607001E4B5E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 2ABCBC312CDEB607001E4B5E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2ABCBC1D2CDEB606001E4B5E /* writing-tools */; + targetProxy = 2ABCBC302CDEB607001E4B5E /* PBXContainerItemProxy */; + }; + 2ABCBC3B2CDEB607001E4B5E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2ABCBC1D2CDEB606001E4B5E /* writing-tools */; + targetProxy = 2ABCBC3A2CDEB607001E4B5E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 2ABCBC412CDEB607001E4B5E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 2ABCBC422CDEB607001E4B5E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + 2ABCBC442CDEB607001E4B5E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "writing-tools/writing_tools.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_ASSET_PATHS = "\"writing-tools/Preview Content\""; + DEVELOPMENT_TEAM = MK2V998W66; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "writing-tools/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "WritingTools "; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.aryamirsepasi.writing-tools"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 2ABCBC452CDEB607001E4B5E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "writing-tools/writing_tools.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_ASSET_PATHS = "\"writing-tools/Preview Content\""; + DEVELOPMENT_TEAM = MK2V998W66; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "writing-tools/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "WritingTools "; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.aryamirsepasi.writing-tools"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 2ABCBC472CDEB607001E4B5E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = MK2V998W66; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 15.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.aryamirsepasi.swift-writing-toolsTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/writing-tools.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/writing-tools"; + }; + name = Debug; + }; + 2ABCBC482CDEB607001E4B5E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = MK2V998W66; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 15.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.aryamirsepasi.swift-writing-toolsTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/writing-tools.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/writing-tools"; + }; + name = Release; + }; + 2ABCBC4A2CDEB607001E4B5E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = MK2V998W66; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.aryamirsepasi.swift-writing-toolsUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = "swift-writing-tools"; + }; + name = Debug; + }; + 2ABCBC4B2CDEB607001E4B5E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = MK2V998W66; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.aryamirsepasi.swift-writing-toolsUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = "swift-writing-tools"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2ABCBC192CDEB606001E4B5E /* Build configuration list for PBXProject "writing-tools" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2ABCBC412CDEB607001E4B5E /* Debug */, + 2ABCBC422CDEB607001E4B5E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2ABCBC432CDEB607001E4B5E /* Build configuration list for PBXNativeTarget "writing-tools" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2ABCBC442CDEB607001E4B5E /* Debug */, + 2ABCBC452CDEB607001E4B5E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2ABCBC462CDEB607001E4B5E /* Build configuration list for PBXNativeTarget "writing-toolsTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2ABCBC472CDEB607001E4B5E /* Debug */, + 2ABCBC482CDEB607001E4B5E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2ABCBC492CDEB607001E4B5E /* Build configuration list for PBXNativeTarget "writing-toolsUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2ABCBC4A2CDEB607001E4B5E /* Debug */, + 2ABCBC4B2CDEB607001E4B5E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 2A5786292CEFB15900DB01FC /* XCRemoteSwiftPackageReference "HotKey" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/soffes/HotKey"; + requirement = { + branch = main; + kind = branch; + }; + }; + 2AA154102CF2636900D27D18 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/gonzalezreal/swift-markdown-ui"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.4.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 2A57862A2CEFB15900DB01FC /* HotKey */ = { + isa = XCSwiftPackageProductDependency; + package = 2A5786292CEFB15900DB01FC /* XCRemoteSwiftPackageReference "HotKey" */; + productName = HotKey; + }; + 2AA154112CF2636900D27D18 /* MarkdownUI */ = { + isa = XCSwiftPackageProductDependency; + package = 2AA154102CF2636900D27D18 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; + productName = MarkdownUI; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 2ABCBC162CDEB606001E4B5E /* Project object */; +} From 1616266cbbc9aeb1df5e27c458a3e02ec5b19411 Mon Sep 17 00:00:00 2001 From: Aryamirsepasi Date: Mon, 6 Jan 2025 01:10:43 +0100 Subject: [PATCH 3/9] replaced custom key recording with 3rd party package + other improvements --- macOS/writing-tools/AppDelegate.swift | 184 ++++++------------ macOS/writing-tools/AppState.swift | 75 ++++--- .../{AI Providers => Models}/AIProvider.swift | 0 macOS/writing-tools/Models/AppSettings.swift | 93 +++++++++ .../GeminiProvider.swift | 0 .../OpenAIProvider.swift | 0 macOS/writing-tools/UI/OnboardingView.swift | 23 +-- macOS/writing-tools/UI/SettingsView.swift | 149 +------------- 8 files changed, 199 insertions(+), 325 deletions(-) rename macOS/writing-tools/{AI Providers => Models}/AIProvider.swift (100%) create mode 100644 macOS/writing-tools/Models/AppSettings.swift rename macOS/writing-tools/{AI Providers => Models}/GeminiProvider.swift (100%) rename macOS/writing-tools/{AI Providers => Models}/OpenAIProvider.swift (100%) diff --git a/macOS/writing-tools/AppDelegate.swift b/macOS/writing-tools/AppDelegate.swift index ad01724..f0e4cdd 100644 --- a/macOS/writing-tools/AppDelegate.swift +++ b/macOS/writing-tools/AppDelegate.swift @@ -1,4 +1,5 @@ import SwiftUI +import KeyboardShortcuts import HotKey import Carbon.HIToolbox @@ -9,7 +10,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { // Property to track service-triggered popups private var isServiceTriggered: Bool = false - + // Computed property to manage the menu bar status item var statusBarItem: NSStatusItem! { get { @@ -36,7 +37,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { func applicationDidFinishLaunching(_ notification: Notification) { NSApp.servicesProvider = self - + if CommandLine.arguments.contains("--reset") { DispatchQueue.main.async { [weak self] in self?.performRecoveryReset() @@ -46,7 +47,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { DispatchQueue.main.async { [weak self] in self?.setupMenuBar() - self?.setupHotKey() if self?.statusBarItem == nil { self?.recreateStatusBarItem() @@ -58,6 +58,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { self?.requestAccessibilityPermissions() } + + KeyboardShortcuts.onKeyUp(for: .showPopup) { [weak self] in + self?.showPopup() + } } // Called when app is about to close - performs cleanup @@ -102,7 +106,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { recreateStatusBarItem() setupMenuBar() - setupHotKey() let alert = NSAlert() alert.messageText = "App Reset Complete" @@ -126,7 +129,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { // Recreate status bar and setup recreateStatusBarItem() setupMenuBar() - setupHotKey() // Show confirmation let alert = NSAlert() @@ -154,70 +156,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { } } - // Initializes keyboard shortcut handling - private func setupHotKey() { - updateHotKey() - - NotificationCenter.default.removeObserver(self, name: UserDefaults.didChangeNotification, object: nil) - NotificationCenter.default.addObserver( - self, - selector: #selector(shortcutChanged), - name: UserDefaults.didChangeNotification, - object: nil - ) - } - - // Handles changes to keyboard shortcut settings - @objc private func shortcutChanged() { - DispatchQueue.main.async { [weak self] in - if UserDefaults.standard.string(forKey: "shortcut") != nil { - self?.updateHotKey() - } - } - } - - // Updates the active keyboard shortcut based on settings - private func updateHotKey() { - // Clear any existing hotKey - hotKey = nil - - // Retrieve raw code & modifiers from UserDefaults - let rawKeyCode = UserDefaults.standard.integer(forKey: "hotKey_keyCode") - let rawModifiers = UserDefaults.standard.integer(forKey: "hotKey_modifiers") - - // If user never recorded anything, set a default. - if rawKeyCode == 0 && rawModifiers == 0 { - // Provide default if needed - let defaultKeyCode = kVK_ANSI_D - let defaultFlags = NSEvent.ModifierFlags.control.carbonFlags - - UserDefaults.standard.set(Int(defaultKeyCode), forKey: "hotKey_keyCode") - UserDefaults.standard.set(Int(defaultFlags), forKey: "hotKey_modifiers") - - // Re-read from UserDefaults so code proceeds - return updateHotKey() - } - - // Construct the HotKey from those raw integers - let carbonKeyCode = UInt32(rawKeyCode) - let carbonModifiers = UInt32(rawModifiers) - - // Create the HotKey instance - hotKey = HotKey(keyCombo: KeyCombo( - carbonKeyCode: carbonKeyCode, - carbonModifiers: carbonModifiers - )) - - hotKey?.keyDownHandler = { [weak self] in - DispatchQueue.main.async { - if let frontmostApp = NSWorkspace.shared.frontmostApplication { - self?.appState.previousApplication = frontmostApp - } - self?.showPopup() - } - } - } - // Shows the first-time setup/onboarding window private func showOnboarding() { let window = NSWindow( @@ -354,73 +292,73 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { // Handles window cleanup when any window is closed func windowWillClose(_ notification: Notification) { - guard !isServiceTriggered else { return } - - guard let window = notification.object as? NSWindow else { return } - DispatchQueue.main.async { [weak self] in - if window == self?.settingsWindow { - self?.settingsHostingView = nil - self?.settingsWindow = nil - } else if window == self?.aboutWindow { - self?.aboutHostingView = nil - self?.aboutWindow = nil - } else if window == self?.popupWindow { - self?.popupWindow?.delegate = nil - self?.popupWindow = nil - } + guard !isServiceTriggered else { return } + + guard let window = notification.object as? NSWindow else { return } + DispatchQueue.main.async { [weak self] in + if window == self?.settingsWindow { + self?.settingsHostingView = nil + self?.settingsWindow = nil + } else if window == self?.aboutWindow { + self?.aboutHostingView = nil + self?.aboutWindow = nil + } else if window == self?.popupWindow { + self?.popupWindow?.delegate = nil + self?.popupWindow = nil } } + } // Service handler for processing selected text @objc func handleSelectedText(_ pboard: NSPasteboard, userData: String, error: AutoreleasingUnsafeMutablePointer) { - let types: [NSPasteboard.PasteboardType] = [ - .string, - .rtf, - NSPasteboard.PasteboardType("public.plain-text") - ] - - guard let selectedText = types.lazy.compactMap({ pboard.string(forType: $0) }).first, - !selectedText.isEmpty else { - error.pointee = "No text was selected" as NSString - return + let types: [NSPasteboard.PasteboardType] = [ + .string, + .rtf, + NSPasteboard.PasteboardType("public.plain-text") + ] + + guard let selectedText = types.lazy.compactMap({ pboard.string(forType: $0) }).first, + !selectedText.isEmpty else { + error.pointee = "No text was selected" as NSString + return + } + + // Store the selected text + appState.selectedText = selectedText + + // Set service trigger flag + isServiceTriggered = true + + // Show the popup + DispatchQueue.main.async { [weak self] in + if let frontmostApp = NSWorkspace.shared.frontmostApplication { + self?.appState.previousApplication = frontmostApp } - // Store the selected text - appState.selectedText = selectedText - - // Set service trigger flag - isServiceTriggered = true + guard let self = self else { return } - // Show the popup - DispatchQueue.main.async { [weak self] in - if let frontmostApp = NSWorkspace.shared.frontmostApplication { - self?.appState.previousApplication = frontmostApp - } + if !selectedText.isEmpty { + let window = PopupWindow(appState: self.appState) + window.delegate = self - guard let self = self else { return } + self.closePopupWindow() + self.popupWindow = window - if !selectedText.isEmpty { - let window = PopupWindow(appState: self.appState) - window.delegate = self - - self.closePopupWindow() - self.popupWindow = window - - // Configure window for service mode - window.level = .floating - window.collectionBehavior = [.moveToActiveSpace] - - window.positionNearMouse() - window.makeKeyAndOrderFront(nil) - window.orderFrontRegardless() - } + // Configure window for service mode + window.level = .floating + window.collectionBehavior = [.moveToActiveSpace] - // Reset the flag after a delay - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.isServiceTriggered = false - } + window.positionNearMouse() + window.makeKeyAndOrderFront(nil) + window.orderFrontRegardless() + } + + // Reset the flag after a delay + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.isServiceTriggered = false } } + } } // Converts SwiftUI modifier flags to Carbon modifier flags for HotKey library diff --git a/macOS/writing-tools/AppState.swift b/macOS/writing-tools/AppState.swift index 4ceb6f0..3361c0a 100644 --- a/macOS/writing-tools/AppState.swift +++ b/macOS/writing-tools/AppState.swift @@ -5,78 +5,73 @@ class AppState: ObservableObject { @Published var geminiProvider: GeminiProvider @Published var openAIProvider: OpenAIProvider - @Published var currentProvider: String // "gemini" or "openai" + @Published var customInstruction: String = "" @Published var selectedText: String = "" @Published var isPopupVisible: Bool = false @Published var isProcessing: Bool = false @Published var previousApplication: NSRunningApplication? + + // Derived from AppSettings + var currentProvider: String { + get { AppSettings.shared.currentProvider } + set { AppSettings.shared.currentProvider = newValue } + } - var activeProvider: (any AIProvider) { - currentProvider == "openai" ? openAIProvider as any AIProvider : geminiProvider as any AIProvider + var activeProvider: any AIProvider { + currentProvider == "openai" ? openAIProvider : geminiProvider } private init() { + // Read from AppSettings + let asettings = AppSettings.shared + // Initialize Gemini - let geminiApiKey = UserDefaults.standard.string(forKey: "gemini_api_key")?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - let geminiModelName = UserDefaults.standard.string(forKey: "gemini_model") ?? GeminiModel.oneflash.rawValue - let geminiConfig = GeminiConfig(apiKey: geminiApiKey, modelName: geminiModelName) + let geminiConfig = GeminiConfig(apiKey: asettings.geminiApiKey, + modelName: asettings.geminiModel.rawValue) self.geminiProvider = GeminiProvider(config: geminiConfig) // Initialize OpenAI - let openAIApiKey = UserDefaults.standard.string(forKey: "openai_api_key")?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - let openAIBaseURL = UserDefaults.standard.string(forKey: "openai_base_url") ?? OpenAIConfig.defaultBaseURL - let openAIOrg = UserDefaults.standard.string(forKey: "openai_organization") - let openAIProject = UserDefaults.standard.string(forKey: "openai_project") - let openAIModel = UserDefaults.standard.string(forKey: "openai_model") ?? OpenAIModel.gpt4.rawValue - let openAIConfig = OpenAIConfig( - apiKey: openAIApiKey, - baseURL: openAIBaseURL, - organization: openAIOrg, - project: openAIProject, - model: openAIModel + apiKey: asettings.openAIApiKey, + baseURL: asettings.openAIBaseURL, + organization: asettings.openAIOrganization, + project: asettings.openAIProject, + model: asettings.openAIModel ) self.openAIProvider = OpenAIProvider(config: openAIConfig) - // Set current provider - self.currentProvider = UserDefaults.standard.string(forKey: "current_provider") ?? "gemini" - - if openAIApiKey.isEmpty && geminiApiKey.isEmpty { + if asettings.openAIApiKey.isEmpty && asettings.geminiApiKey.isEmpty { print("Warning: No API keys configured.") } } - // Save Gemini API configuration + // For Gemini changes func saveGeminiConfig(apiKey: String, model: GeminiModel) { - UserDefaults.standard.setValue(apiKey, forKey: "gemini_api_key") - UserDefaults.standard.setValue(model.rawValue, forKey: "gemini_model") + AppSettings.shared.geminiApiKey = apiKey + AppSettings.shared.geminiModel = model let config = GeminiConfig(apiKey: apiKey, modelName: model.rawValue) geminiProvider = GeminiProvider(config: config) } - // Save OpenAI API configuration + // For OpenAI changes func saveOpenAIConfig(apiKey: String, baseURL: String, organization: String?, project: String?, model: String) { - UserDefaults.standard.setValue(apiKey, forKey: "openai_api_key") - UserDefaults.standard.setValue(baseURL, forKey: "openai_base_url") - UserDefaults.standard.setValue(organization, forKey: "openai_organization") - UserDefaults.standard.setValue(project, forKey: "openai_project") - UserDefaults.standard.setValue(model, forKey: "openai_model") + let asettings = AppSettings.shared + asettings.openAIApiKey = apiKey + asettings.openAIBaseURL = baseURL + asettings.openAIOrganization = organization + asettings.openAIProject = project + asettings.openAIModel = model - let config = OpenAIConfig( - apiKey: apiKey, - baseURL: baseURL, - organization: organization, - project: project, - model: model - ) + let config = OpenAIConfig(apiKey: apiKey, baseURL: baseURL, + organization: organization, project: project, + model: model) openAIProvider = OpenAIProvider(config: config) } - // Update the current AI provider + // Switch AI provider func setCurrentProvider(_ provider: String) { - currentProvider = provider - UserDefaults.standard.setValue(provider, forKey: "current_provider") + AppSettings.shared.currentProvider = provider } } diff --git a/macOS/writing-tools/AI Providers/AIProvider.swift b/macOS/writing-tools/Models/AIProvider.swift similarity index 100% rename from macOS/writing-tools/AI Providers/AIProvider.swift rename to macOS/writing-tools/Models/AIProvider.swift diff --git a/macOS/writing-tools/Models/AppSettings.swift b/macOS/writing-tools/Models/AppSettings.swift new file mode 100644 index 0000000..9d31c42 --- /dev/null +++ b/macOS/writing-tools/Models/AppSettings.swift @@ -0,0 +1,93 @@ +import Foundation + +// A singleton for app-wide settings that wraps UserDefaults access +class AppSettings: ObservableObject { + static let shared = AppSettings() + + private let defaults = UserDefaults.standard + + // MARK: - Published Settings + @Published var geminiApiKey: String { + didSet { defaults.set(geminiApiKey, forKey: "gemini_api_key") } + } + + @Published var geminiModel: GeminiModel { + didSet { defaults.set(geminiModel.rawValue, forKey: "gemini_model") } + } + + @Published var openAIApiKey: String { + didSet { defaults.set(openAIApiKey, forKey: "openai_api_key") } + } + + @Published var openAIBaseURL: String { + didSet { defaults.set(openAIBaseURL, forKey: "openai_base_url") } + } + + @Published var openAIModel: String { + didSet { defaults.set(openAIModel, forKey: "openai_model") } + } + + @Published var openAIOrganization: String? { + didSet { defaults.set(openAIOrganization, forKey: "openai_organization") } + } + + @Published var openAIProject: String? { + didSet { defaults.set(openAIProject, forKey: "openai_project") } + } + + @Published var currentProvider: String { + didSet { defaults.set(currentProvider, forKey: "current_provider") } + } + + @Published var shortcutText: String { + didSet { defaults.set(shortcutText, forKey: "shortcut") } + } + + @Published var hasCompletedOnboarding: Bool { + didSet { defaults.set(hasCompletedOnboarding, forKey: "has_completed_onboarding") } + } + + @Published var useGradientTheme: Bool { + didSet { defaults.set(useGradientTheme, forKey: "use_gradient_theme") } + } + + // MARK: - HotKey data + @Published var hotKeyCode: Int { + didSet { defaults.set(hotKeyCode, forKey: "hotKey_keyCode") } + } + @Published var hotKeyModifiers: Int { + didSet { defaults.set(hotKeyModifiers, forKey: "hotKey_modifiers") } + } + + // MARK: - Init + private init() { + let defaults = UserDefaults.standard + + // Load or set defaults + self.geminiApiKey = defaults.string(forKey: "gemini_api_key") ?? "" + let geminiModelStr = defaults.string(forKey: "gemini_model") ?? GeminiModel.oneflash.rawValue + self.geminiModel = GeminiModel(rawValue: geminiModelStr) ?? .oneflash + + self.openAIApiKey = defaults.string(forKey: "openai_api_key") ?? "" + self.openAIBaseURL = defaults.string(forKey: "openai_base_url") ?? OpenAIConfig.defaultBaseURL + self.openAIModel = defaults.string(forKey: "openai_model") ?? OpenAIConfig.defaultModel + self.openAIOrganization = defaults.string(forKey: "openai_organization") ?? nil + self.openAIProject = defaults.string(forKey: "openai_project") ?? nil + + self.currentProvider = defaults.string(forKey: "current_provider") ?? "gemini" + self.shortcutText = defaults.string(forKey: "shortcut") ?? "⌥ Space" + self.hasCompletedOnboarding = defaults.bool(forKey: "has_completed_onboarding") + self.useGradientTheme = defaults.bool(forKey: "use_gradient_theme") + + // HotKey + self.hotKeyCode = defaults.integer(forKey: "hotKey_keyCode") + self.hotKeyModifiers = defaults.integer(forKey: "hotKey_modifiers") + } + + // MARK: - Convenience + func resetAll() { + let domain = Bundle.main.bundleIdentifier! + UserDefaults.standard.removePersistentDomain(forName: domain) + UserDefaults.standard.synchronize() + } +} diff --git a/macOS/writing-tools/AI Providers/GeminiProvider.swift b/macOS/writing-tools/Models/GeminiProvider.swift similarity index 100% rename from macOS/writing-tools/AI Providers/GeminiProvider.swift rename to macOS/writing-tools/Models/GeminiProvider.swift diff --git a/macOS/writing-tools/AI Providers/OpenAIProvider.swift b/macOS/writing-tools/Models/OpenAIProvider.swift similarity index 100% rename from macOS/writing-tools/AI Providers/OpenAIProvider.swift rename to macOS/writing-tools/Models/OpenAIProvider.swift diff --git a/macOS/writing-tools/UI/OnboardingView.swift b/macOS/writing-tools/UI/OnboardingView.swift index a1fc557..eab13a7 100644 --- a/macOS/writing-tools/UI/OnboardingView.swift +++ b/macOS/writing-tools/UI/OnboardingView.swift @@ -1,9 +1,10 @@ import SwiftUI +import KeyboardShortcuts struct OnboardingView: View { @ObservedObject var appState: AppState @State private var currentStep = 0 - @State private var shortcutText = "⌘ Space" + @State private var shortcutText = "⌃ Space" @State private var useGradientTheme = true @State private var isShowingSettings = false @@ -141,7 +142,7 @@ struct OnboardingView: View { private var customizationStep: some View { VStack(spacing: 20) { - Text(steps[2].title) + Text("Customize Your Experience") .font(.title) .bold() @@ -149,29 +150,15 @@ struct OnboardingView: View { Text("Set your keyboard shortcut:") .font(.headline) - ShortcutRecorderView() - .frame(maxWidth: .infinity) - - Text("Important: For reliable shortcuts, try:") - .font(.subheadline) - .foregroundColor(.secondary) - - Text("• Control (⌃) + Letter (e.g., ⌃ D)") - Text("• Option (⌥) + Space") - Text("• Command (⌘) + Letter") - - Divider() - - Text("Choose your theme:") - .font(.headline) + KeyboardShortcuts.Recorder("Shortcut:", name: .showPopup) Toggle("Use Gradient Theme", isOn: $useGradientTheme) } } } + private func saveSettingsAndContinue() { - UserDefaults.standard.set(shortcutText, forKey: "shortcut") UserDefaults.standard.set(useGradientTheme, forKey: "use_gradient_theme") WindowManager.shared.transitonFromOnboardingToSettings(appState: appState) } diff --git a/macOS/writing-tools/UI/SettingsView.swift b/macOS/writing-tools/UI/SettingsView.swift index d49ea58..3ec1625 100644 --- a/macOS/writing-tools/UI/SettingsView.swift +++ b/macOS/writing-tools/UI/SettingsView.swift @@ -1,137 +1,9 @@ import SwiftUI import Carbon.HIToolbox +import KeyboardShortcuts -struct ShortcutRecorderView: View { - @State private var displayText = "" - - @FocusState private var isFocused: Bool - @State private var isRecording = false - - var body: some View { - HStack { - Text(displayText.isEmpty ? "Click to record" : displayText) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(8) - .background(Color(.textBackgroundColor)) - .cornerRadius(6) - .onTapGesture { - isFocused = true - isRecording = true - displayText = "Recording..." - } - - if isRecording { - Text("Press desired shortcut…") - .foregroundColor(.secondary) - } - } - .focusable() - .focused($isFocused) - .onAppear { - NSEvent.addLocalMonitorForEvents(matching: [.keyDown, .flagsChanged]) { event in - if isRecording { - handleKeyEvent(event) - // Return nil to indicate the event is handled - return nil - } - // Otherwise, pass it on - return event - } - } - } - - private func handleKeyEvent(_ event: NSEvent) { - if event.type == .keyDown { - // Modifiers - let carbonModifiers = event.modifierFlags.intersection(.deviceIndependentFlagsMask).carbonFlags - - // The physical key code from the system - let rawKeyCode = UInt32(event.keyCode) - - // Save them in UserDefaults - UserDefaults.standard.set(Int(rawKeyCode), forKey: "hotKey_keyCode") - UserDefaults.standard.set(Int(carbonModifiers), forKey: "hotKey_modifiers") - - displayText = describeShortcut(keyCode: event.keyCode, - flags: event.modifierFlags) - - // Done recording - isRecording = false - isFocused = false - } - } - - // helper to produce a “Ctrl + D” style string. - private func describeShortcut(keyCode: UInt16, flags: NSEvent.ModifierFlags) -> String { - var parts: [String] = [] - - // 1) Collect modifier flags - if flags.contains(.command) { parts.append("⌘") } - if flags.contains(.option) { parts.append("⌥") } - if flags.contains(.control) { parts.append("⌃") } - if flags.contains(.shift) { parts.append("⇧") } - - // 2) Convert keyCode -> Int - let keyCodeInt = Int(keyCode) - - // 3) Check if it matches certain special/symbol keys - switch keyCodeInt { - case kVK_Space: - parts.append("Space") - case kVK_Return: - parts.append("Return") - case kVK_ANSI_Equal: - parts.append("=") - case kVK_ANSI_Minus: - parts.append("-") - case kVK_ANSI_LeftBracket: - parts.append("[") - case kVK_ANSI_RightBracket: - parts.append("]") - // Add more symbol keys if needed (e.g., kVK_ANSI_Semicolon, etc.) - - default: - // 4) If we find a letter in our dictionary, use it; else show the numeric code. - if let letter = keyCodeToLetter[keyCodeInt] { - parts.append(letter) - } else { - parts.append("(\(keyCode))") // Fallback for anything unrecognized - } - } - - // 5) Combine with spaces, e.g. "⌃ D", "⌘ =" - return parts.joined(separator: " ") - } - - // Maps the Carbon virtual key code (e.g. kVK_ANSI_D = 0x02) to the actual letter "D". - private let keyCodeToLetter: [Int: String] = [ - kVK_ANSI_A: "A", - kVK_ANSI_B: "B", - kVK_ANSI_C: "C", - kVK_ANSI_D: "D", - kVK_ANSI_E: "E", - kVK_ANSI_F: "F", - kVK_ANSI_G: "G", - kVK_ANSI_H: "H", - kVK_ANSI_I: "I", - kVK_ANSI_J: "J", - kVK_ANSI_K: "K", - kVK_ANSI_L: "L", - kVK_ANSI_M: "M", - kVK_ANSI_N: "N", - kVK_ANSI_O: "O", - kVK_ANSI_P: "P", - kVK_ANSI_Q: "Q", - kVK_ANSI_R: "R", - kVK_ANSI_S: "S", - kVK_ANSI_T: "T", - kVK_ANSI_U: "U", - kVK_ANSI_V: "V", - kVK_ANSI_W: "W", - kVK_ANSI_X: "X", - kVK_ANSI_Y: "Y", - kVK_ANSI_Z: "Z" - ] +extension KeyboardShortcuts.Name { + static let showPopup = Self("showPopup") } struct SettingsView: View { @@ -184,22 +56,11 @@ struct SettingsView: View { if !showOnlyApiSetup { Section("General Settings") { Form { - Text(displayShortcut.isEmpty ? "Not set" : displayShortcut) - - ShortcutRecorderView() - } - .onAppear { - // Load the raw keyCode & modifiers from UserDefaults - let rawKeyCode = UserDefaults.standard.integer(forKey: "hotKey_keyCode") - let rawModifiers = UserDefaults.standard.integer(forKey: "hotKey_modifiers") + KeyboardShortcuts.Recorder("Global Shortcut:", name: .showPopup) - // Convert to proper Swift types - let keyCode = UInt16(rawKeyCode) - let flags = decodeCarbonModifiers(rawModifiers) - - displayShortcut = describeShortcut(keyCode: keyCode, flags: flags) } + Toggle("Use Gradient Theme", isOn: $useGradientTheme) } From 626d0bf6a57523a423978d3be975b00437bd3bfc Mon Sep 17 00:00:00 2001 From: Aryamirsepasi Date: Sun, 12 Jan 2025 11:18:49 +0100 Subject: [PATCH 4/9] thanks to @Joaov41 for fixing the popup closing, added some improvements to chat window --- macOS/writing-tools/AppDelegate.swift | 4 -- .../UI/Custom Modifiers/ChatBubble.swift | 10 ++++ .../Custom Modifiers/ChatBubbleModifier.swift | 20 +++++++ macOS/writing-tools/UI/PopupView.swift | 5 +- macOS/writing-tools/UI/ResponseView.swift | 54 +++++++++++-------- 5 files changed, 66 insertions(+), 27 deletions(-) create mode 100644 macOS/writing-tools/UI/Custom Modifiers/ChatBubble.swift create mode 100644 macOS/writing-tools/UI/Custom Modifiers/ChatBubbleModifier.swift diff --git a/macOS/writing-tools/AppDelegate.swift b/macOS/writing-tools/AppDelegate.swift index f0e4cdd..a21f7bd 100644 --- a/macOS/writing-tools/AppDelegate.swift +++ b/macOS/writing-tools/AppDelegate.swift @@ -1,6 +1,5 @@ import SwiftUI import KeyboardShortcuts -import HotKey import Carbon.HIToolbox class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { @@ -24,7 +23,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { AppDelegate.sharedStatusItem = newValue } } - var hotKey: HotKey? let appState = AppState.shared private var settingsWindow: NSWindow? private var aboutWindow: NSWindow? @@ -100,7 +98,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { // Resets app to default state when triggered from menu @objc private func resetApp() { - hotKey = nil WindowManager.shared.cleanupWindows() recreateStatusBarItem() @@ -123,7 +120,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { UserDefaults.standard.synchronize() // Reset the app state - hotKey = nil WindowManager.shared.cleanupWindows() // Recreate status bar and setup diff --git a/macOS/writing-tools/UI/Custom Modifiers/ChatBubble.swift b/macOS/writing-tools/UI/Custom Modifiers/ChatBubble.swift new file mode 100644 index 0000000..9a4ece1 --- /dev/null +++ b/macOS/writing-tools/UI/Custom Modifiers/ChatBubble.swift @@ -0,0 +1,10 @@ +import SwiftUI + +struct ChatBubble: Shape { + var isFromUser: Bool + + func path(in rect: CGRect) -> Path { + // A simple rounded rect with a corner radius + return RoundedRectangle(cornerRadius: 12, style: .continuous) + .path(in: rect) } +} diff --git a/macOS/writing-tools/UI/Custom Modifiers/ChatBubbleModifier.swift b/macOS/writing-tools/UI/Custom Modifiers/ChatBubbleModifier.swift new file mode 100644 index 0000000..b34bb7d --- /dev/null +++ b/macOS/writing-tools/UI/Custom Modifiers/ChatBubbleModifier.swift @@ -0,0 +1,20 @@ +import SwiftUI + +struct ChatBubbleModifier: ViewModifier { + let isFromUser: Bool + + func body(content: Content) -> some View { + content + .padding() + .background( + ChatBubble(isFromUser: isFromUser) + .fill(isFromUser ? Color.blue.opacity(0.15) : Color(.controlBackgroundColor)) + ) + } +} + +extension View { + func chatBubbleStyle(isFromUser: Bool) -> some View { + self.modifier(ChatBubbleModifier(isFromUser: isFromUser)) + } +} diff --git a/macOS/writing-tools/UI/PopupView.swift b/macOS/writing-tools/UI/PopupView.swift index 968f25f..9b05aaa 100644 --- a/macOS/writing-tools/UI/PopupView.swift +++ b/macOS/writing-tools/UI/PopupView.swift @@ -94,6 +94,7 @@ struct PopupView: View { } } + // Process custom commands private func processCustomCommand(_ command: CustomCommand) { loadingOptions.insert(command.id.uuidString) appState.isProcessing = true @@ -169,6 +170,8 @@ struct PopupView: View { await MainActor.run { showResponseWindow(for: option, with: result) } + // Close the popup window after showing the response window + closeAction() } else { NSPasteboard.general.clearContents() NSPasteboard.general.setString(result, forType: .string) @@ -202,7 +205,7 @@ struct PopupView: View { """ let userPrompt = appState.selectedText.isEmpty ? - instruction : + instruction : """ User's instruction: \(instruction) diff --git a/macOS/writing-tools/UI/ResponseView.swift b/macOS/writing-tools/UI/ResponseView.swift index 116c271..94f52a3 100644 --- a/macOS/writing-tools/UI/ResponseView.swift +++ b/macOS/writing-tools/UI/ResponseView.swift @@ -117,37 +117,47 @@ struct ResponseView: View { } struct ChatMessageView: View { + let message: ChatMessage let fontSize: CGFloat - @Environment(\.colorScheme) var colorScheme var body: some View { - HStack(alignment: .top) { - if message.role == "user" { - Spacer(minLength: 60) - } - - VStack(alignment: message.role == "user" ? .trailing : .leading, spacing: 4) { - ViewThatFits(in: .horizontal) { - Markdown(message.content) - .font(.system(size: fontSize)) - .textSelection(.enabled) - .padding() - .frame(minWidth: 100, idealWidth: 450, maxWidth: 600, alignment: .leading) - .background(message.role == "user" ? Color.accentColor.opacity(0.1) : Color(.controlBackgroundColor)) - .cornerRadius(12) - } - - Text(message.timestamp.formatted(.dateTime.hour().minute())) - .font(.caption2) - .foregroundColor(.secondary) - } + // If user message is on the right, assistant on the left: + HStack(alignment: .top, spacing: 12) { + // If it's assistant, push bubble to the left if message.role == "assistant" { - Spacer(minLength: 60) + bubbleView(role: message.role) + Spacer(minLength: 15) + } else { + Spacer(minLength: 15) + bubbleView(role: message.role) } } + .padding(.top, 4) } + + @ViewBuilder + private func bubbleView(role: String) -> some View { + VStack(alignment: role == "assistant" ? .leading : .trailing, spacing: 2) { + Markdown(message.content) + .font(.system(size: fontSize)) + .textSelection(.enabled) + .chatBubbleStyle(isFromUser: message.role == "user") + + // Time stamp + Text(message.timestamp.formatted(.dateTime.hour().minute())) + .font(.caption2) + .foregroundColor(.secondary) + .padding(.bottom, 2) + } + .frame(maxWidth: 500, alignment: role == "assistant" ? .leading : .trailing) + } +} + +// A small convenience enum for clarity (optional) +fileprivate enum MessageRole { + case user, assistant } From 13ffe0dca87ef2f0cb8cc6dfeaffc52e568d4291 Mon Sep 17 00:00:00 2001 From: Aryamirsepasi Date: Sun, 12 Jan 2025 17:16:50 +0100 Subject: [PATCH 5/9] fixed window activation bug + added Glassmorphic theme --- macOS/writing-tools/AppDelegate.swift | 5 ++ .../Custom Modifiers/BackgroundModifier.swift | 53 +++++++++++++++++-- macOS/writing-tools/UI/OnboardingView.swift | 17 +++++- macOS/writing-tools/UI/PopupView.swift | 12 ++++- macOS/writing-tools/UI/SettingsView.swift | 17 +++++- 5 files changed, 94 insertions(+), 10 deletions(-) diff --git a/macOS/writing-tools/AppDelegate.swift b/macOS/writing-tools/AppDelegate.swift index a21f7bd..0f30442 100644 --- a/macOS/writing-tools/AppDelegate.swift +++ b/macOS/writing-tools/AppDelegate.swift @@ -229,6 +229,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { DispatchQueue.main.async { [weak self] in guard let self = self else { return } + // Store the current frontmost application before showing popup + if let frontmostApp = NSWorkspace.shared.frontmostApplication { + self.appState.previousApplication = frontmostApp + } + self.closePopupWindow() let pasteboard = NSPasteboard.general diff --git a/macOS/writing-tools/UI/Custom Modifiers/BackgroundModifier.swift b/macOS/writing-tools/UI/Custom Modifiers/BackgroundModifier.swift index 3532acd..5982607 100644 --- a/macOS/writing-tools/UI/Custom Modifiers/BackgroundModifier.swift +++ b/macOS/writing-tools/UI/Custom Modifiers/BackgroundModifier.swift @@ -1,14 +1,30 @@ import SwiftUI +enum AppTheme: String { + case standard = "standard" + case gradient = "gradient" + case glass = "glass" +} + struct WindowBackground: ViewModifier { @Environment(\.colorScheme) var colorScheme let useGradient: Bool + var currentTheme: AppTheme { + if !useGradient { + return .standard + } + return UserDefaults.standard.string(forKey: "theme_style") == "glass" ? .glass : .gradient + } + func body(content: Content) -> some View { content .background( Group { - if useGradient { + switch currentTheme { + case .standard: + Color(.windowBackgroundColor) + case .gradient: if colorScheme == .light { LinearGradient( colors: [ @@ -32,14 +48,43 @@ struct WindowBackground: ViewModifier { endPoint: .bottomTrailing ) } - } else { - Color(.windowBackgroundColor) + case .glass: + GlassmorphicBackground() } } ) } } +struct GlassmorphicBackground: View { + @Environment(\.colorScheme) var colorScheme + + var body: some View { + ZStack { + // Base color and Gradient overlay in light mode (Dark mode looks good) + if (colorScheme == .light){ + Color(.windowBackgroundColor) + .opacity(0.2) + + LinearGradient( + colors: [ + Color.white.opacity(0.1), + Color.white.opacity(0.2) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + + } + + // Blur effect + Rectangle() + .fill(.ultraThinMaterial) + + } + } +} + extension Color { init(hex: String) { let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) @@ -60,7 +105,7 @@ extension Color { .sRGB, red: Double(r) / 255, green: Double(g) / 255, - blue: Double(b) / 255, + blue: Double(b) / 255, opacity: Double(a) / 255 ) } diff --git a/macOS/writing-tools/UI/OnboardingView.swift b/macOS/writing-tools/UI/OnboardingView.swift index eab13a7..4eadf8e 100644 --- a/macOS/writing-tools/UI/OnboardingView.swift +++ b/macOS/writing-tools/UI/OnboardingView.swift @@ -6,6 +6,7 @@ struct OnboardingView: View { @State private var currentStep = 0 @State private var shortcutText = "⌃ Space" @State private var useGradientTheme = true + @State private var selectedTheme = UserDefaults.standard.string(forKey: "theme_style") ?? "gradient" @State private var isShowingSettings = false private let steps = [ @@ -152,14 +153,26 @@ struct OnboardingView: View { KeyboardShortcuts.Recorder("Shortcut:", name: .showPopup) - Toggle("Use Gradient Theme", isOn: $useGradientTheme) + Section("Appearance") { + Picker("Theme", selection: $selectedTheme) { + Text("Standard").tag("standard") + Text("Gradient").tag("gradient") + Text("Glass").tag("glass") + } + .pickerStyle(.segmented) + .onChange(of: selectedTheme) { _, newValue in + UserDefaults.standard.set(newValue, forKey: "theme_style") + useGradientTheme = (newValue != "standard") + } + } } } } private func saveSettingsAndContinue() { - UserDefaults.standard.set(useGradientTheme, forKey: "use_gradient_theme") + UserDefaults.standard.set(selectedTheme, forKey: "theme_style") + UserDefaults.standard.set(selectedTheme != "standard", forKey: "use_gradient_theme") WindowManager.shared.transitonFromOnboardingToSettings(appState: appState) } } diff --git a/macOS/writing-tools/UI/PopupView.swift b/macOS/writing-tools/UI/PopupView.swift index 9b05aaa..1ebe49d 100644 --- a/macOS/writing-tools/UI/PopupView.swift +++ b/macOS/writing-tools/UI/PopupView.swift @@ -178,8 +178,16 @@ struct PopupView: View { closeAction() - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - simulatePaste() + // Reactivate previous application and paste + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + if let previousApp = appState.previousApplication { + previousApp.activate() + + // Wait for activation before pasting + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + simulatePaste() + } + } } } } catch { diff --git a/macOS/writing-tools/UI/SettingsView.swift b/macOS/writing-tools/UI/SettingsView.swift index 3ec1625..9189cf2 100644 --- a/macOS/writing-tools/UI/SettingsView.swift +++ b/macOS/writing-tools/UI/SettingsView.swift @@ -10,6 +10,7 @@ struct SettingsView: View { @ObservedObject var appState: AppState @State private var shortcutText = UserDefaults.standard.string(forKey: "shortcut") ?? "⌥ Space" @State private var useGradientTheme = UserDefaults.standard.bool(forKey: "use_gradient_theme") + @State private var selectedTheme = UserDefaults.standard.string(forKey: "theme_style") ?? "gradient" @State private var selectedProvider = UserDefaults.standard.string(forKey: "current_provider") ?? "gemini" // Gemini settings @@ -61,7 +62,18 @@ struct SettingsView: View { } - Toggle("Use Gradient Theme", isOn: $useGradientTheme) + Section("Appearance") { + Picker("Theme", selection: $selectedTheme) { + Text("Standard").tag("standard") + Text("Gradient").tag("gradient") + Text("Glass").tag("glass") + } + .pickerStyle(.segmented) + .onChange(of: selectedTheme) { _, newValue in + UserDefaults.standard.set(newValue, forKey: "theme_style") + useGradientTheme = (newValue != "standard") + } + } } Section("AI Provider") { @@ -136,7 +148,8 @@ struct SettingsView: View { let oldShortcut = UserDefaults.standard.string(forKey: "shortcut") UserDefaults.standard.set(shortcutText, forKey: "shortcut") - UserDefaults.standard.set(useGradientTheme, forKey: "use_gradient_theme") + UserDefaults.standard.set(selectedTheme, forKey: "theme_style") + UserDefaults.standard.set(selectedTheme != "standard", forKey: "use_gradient_theme") // Save provider-specific settings if selectedProvider == "gemini" { From 35dfcf8363ff9cca7c08483531819ca3febd76a9 Mon Sep 17 00:00:00 2001 From: Aryamirsepasi Date: Sun, 12 Jan 2025 17:18:00 +0100 Subject: [PATCH 6/9] added remaining files --- macOS/README.md | 4 +++ macOS/writing-tools.xcodeproj/project.pbxproj | 32 +++++++++---------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/macOS/README.md b/macOS/README.md index 1e7f742..e523226 100644 --- a/macOS/README.md +++ b/macOS/README.md @@ -104,3 +104,7 @@ If you encounter the "Could not open file" error: The macOS port is being developed by **Aryamirsepasi**. GitHub: [https://github.com/Aryamirsepasi](https://github.com/Aryamirsepasi) + +Special Thanks to @sindresorhus for developing an amazing and stable keyboard shortcuts package for Swift. + +GitHub: [https://github.com/sindresorhus/KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts) \ No newline at end of file diff --git a/macOS/writing-tools.xcodeproj/project.pbxproj b/macOS/writing-tools.xcodeproj/project.pbxproj index fde02d5..23203d8 100644 --- a/macOS/writing-tools.xcodeproj/project.pbxproj +++ b/macOS/writing-tools.xcodeproj/project.pbxproj @@ -7,8 +7,8 @@ objects = { /* Begin PBXBuildFile section */ - 2A57862B2CEFB15900DB01FC /* HotKey in Frameworks */ = {isa = PBXBuildFile; productRef = 2A57862A2CEFB15900DB01FC /* HotKey */; }; 2AA154122CF2636900D27D18 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 2AA154112CF2636900D27D18 /* MarkdownUI */; }; + 2AF5CC562D2AFE1C0050F923 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = 2AF5CC552D2AFE1C0050F923 /* KeyboardShortcuts */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,8 +60,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 2A57862B2CEFB15900DB01FC /* HotKey in Frameworks */, 2AA154122CF2636900D27D18 /* MarkdownUI in Frameworks */, + 2AF5CC562D2AFE1C0050F923 /* KeyboardShortcuts in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -120,8 +120,8 @@ ); name = "writing-tools"; packageProductDependencies = ( - 2A57862A2CEFB15900DB01FC /* HotKey */, 2AA154112CF2636900D27D18 /* MarkdownUI */, + 2AF5CC552D2AFE1C0050F923 /* KeyboardShortcuts */, ); productName = "swift-writing-tools"; productReference = 2ABCBC1E2CDEB606001E4B5E /* writing-tools.app */; @@ -200,8 +200,8 @@ mainGroup = 2ABCBC152CDEB606001E4B5E; minimizedProjectReferenceProxies = 1; packageReferences = ( - 2A5786292CEFB15900DB01FC /* XCRemoteSwiftPackageReference "HotKey" */, 2AA154102CF2636900D27D18 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, + 2AF5CC542D2AFE1C0050F923 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */, ); preferredProjectObjectVersion = 77; productRefGroup = 2ABCBC1F2CDEB606001E4B5E /* Products */; @@ -581,35 +581,35 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 2A5786292CEFB15900DB01FC /* XCRemoteSwiftPackageReference "HotKey" */ = { + 2AA154102CF2636900D27D18 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/soffes/HotKey"; + repositoryURL = "https://github.com/gonzalezreal/swift-markdown-ui"; requirement = { - branch = main; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 2.4.1; }; }; - 2AA154102CF2636900D27D18 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = { + 2AF5CC542D2AFE1C0050F923 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/gonzalezreal/swift-markdown-ui"; + repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.4.1; + minimumVersion = 2.2.4; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 2A57862A2CEFB15900DB01FC /* HotKey */ = { - isa = XCSwiftPackageProductDependency; - package = 2A5786292CEFB15900DB01FC /* XCRemoteSwiftPackageReference "HotKey" */; - productName = HotKey; - }; 2AA154112CF2636900D27D18 /* MarkdownUI */ = { isa = XCSwiftPackageProductDependency; package = 2AA154102CF2636900D27D18 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; productName = MarkdownUI; }; + 2AF5CC552D2AFE1C0050F923 /* KeyboardShortcuts */ = { + isa = XCSwiftPackageProductDependency; + package = 2AF5CC542D2AFE1C0050F923 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */; + productName = KeyboardShortcuts; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 2ABCBC162CDEB606001E4B5E /* Project object */; From b27d7b3a7eb44166c5b3a6e2c85adc961b56f3a2 Mon Sep 17 00:00:00 2001 From: Aryamirsepasi Date: Sun, 12 Jan 2025 17:25:50 +0100 Subject: [PATCH 7/9] fixed settings and abour window opening behind other windows --- macOS/writing-tools/AppDelegate.swift | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/macOS/writing-tools/AppDelegate.swift b/macOS/writing-tools/AppDelegate.swift index 0f30442..768dfed 100644 --- a/macOS/writing-tools/AppDelegate.swift +++ b/macOS/writing-tools/AppDelegate.swift @@ -195,7 +195,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { settingsWindow?.contentView = settingsHostingView settingsWindow?.delegate = self - settingsWindow?.makeKeyAndOrderFront(nil) + // Ensure window appears in front + if let window = settingsWindow { + window.level = .floating + NSApp.activate() + window.makeKeyAndOrderFront(nil) + window.orderFrontRegardless() + } } // Opens the about window @@ -219,7 +225,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { aboutWindow?.contentView = aboutHostingView aboutWindow?.delegate = self - aboutWindow?.makeKeyAndOrderFront(nil) + // Ensure window appears in front + if let window = aboutWindow { + window.level = .floating + NSApp.activate(ignoringOtherApps: true) + window.makeKeyAndOrderFront(nil) + window.orderFrontRegardless() + } } // Shows the main popup window when shortcut is triggered From d4a22c0120cb6e07f5c6e7cd1dac1a9134c5c78b Mon Sep 17 00:00:00 2001 From: Aryamirsepasi Date: Mon, 13 Jan 2025 11:44:46 +0100 Subject: [PATCH 8/9] added update checker --- macOS/writing-tools/AppDelegate.swift | 55 ++++++++++++-------- macOS/writing-tools/UI/AboutView.swift | 33 ++++++++++-- macOS/writing-tools/UpdateChecker.swift | 68 +++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 25 deletions(-) create mode 100644 macOS/writing-tools/UpdateChecker.swift diff --git a/macOS/writing-tools/AppDelegate.swift b/macOS/writing-tools/AppDelegate.swift index 768dfed..62e6456 100644 --- a/macOS/writing-tools/AppDelegate.swift +++ b/macOS/writing-tools/AppDelegate.swift @@ -336,6 +336,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { return } + // Store the current frontmost application + if let frontmostApp = NSWorkspace.shared.frontmostApplication { + appState.previousApplication = frontmostApp + } + // Store the selected text appState.selectedText = selectedText @@ -344,29 +349,26 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { // Show the popup DispatchQueue.main.async { [weak self] in - if let frontmostApp = NSWorkspace.shared.frontmostApplication { - self?.appState.previousApplication = frontmostApp - } - guard let self = self else { return } - if !selectedText.isEmpty { - let window = PopupWindow(appState: self.appState) - window.delegate = self - - self.closePopupWindow() - self.popupWindow = window - - // Configure window for service mode - window.level = .floating - window.collectionBehavior = [.moveToActiveSpace] - - window.positionNearMouse() - window.makeKeyAndOrderFront(nil) - window.orderFrontRegardless() - } + let window = PopupWindow(appState: self.appState) + window.delegate = self - // Reset the flag after a delay + self.closePopupWindow() + self.popupWindow = window + + // Configure window for service mode + window.level = .floating + window.collectionBehavior = [.moveToActiveSpace] + + window.positionNearMouse() + window.makeKeyAndOrderFront(nil) + window.orderFrontRegardless() + + // Activate our app + NSApp.activate() + + // Reset the service trigger flag after a delay DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.isServiceTriggered = false } @@ -385,3 +387,16 @@ extension NSEvent.ModifierFlags { return carbon } } + +// extension to support service registration +extension AppDelegate { + override func awakeFromNib() { + super.awakeFromNib() + + // Register services provider + NSApp.servicesProvider = self + + // Register the service + NSUpdateDynamicServices() + } +} diff --git a/macOS/writing-tools/UI/AboutView.swift b/macOS/writing-tools/UI/AboutView.swift index 82d24c7..f23986e 100644 --- a/macOS/writing-tools/UI/AboutView.swift +++ b/macOS/writing-tools/UI/AboutView.swift @@ -2,6 +2,7 @@ import SwiftUI struct AboutView: View { @State private var useGradientTheme = UserDefaults.standard.bool(forKey: "use_gradient_theme") + @State private var updateChecker = UpdateChecker.shared var body: some View { VStack(spacing: 20) { @@ -31,18 +32,40 @@ struct AboutView: View { Link("Email: developer@aryamirsepasi.com", destination: URL(string: "mailto:developer@aryamirsepasi.com")!) - } Divider() - Text("Version: Beta 6 (Based on Windows Port version 6.0)") + Text("Version: 1.0 (Based on Windows Port version 6.0)") .font(.caption) - Button("Check for Updates") { - NSWorkspace.shared.open(URL(string: "https://github.com/theJayTea/WritingTools/releases")!) + // Update checker section + VStack(spacing: 8) { + if updateChecker.isCheckingForUpdates { + ProgressView("Checking for updates...") + } else if let error = updateChecker.checkError { + Text(error) + .foregroundColor(.red) + .font(.caption) + } else if updateChecker.updateAvailable { + Text("A new version is available!") + .foregroundColor(.green) + .font(.caption) + } + + Button(action: { + if updateChecker.updateAvailable { + updateChecker.openReleasesPage() + } else { + Task { + await updateChecker.checkForUpdates() + } + } + }) { + Text(updateChecker.updateAvailable ? "Download Update" : "Check for Updates") + } + .buttonStyle(.borderedProminent) } - .buttonStyle(.borderedProminent) } .padding() .frame(width: 400, height: 400) diff --git a/macOS/writing-tools/UpdateChecker.swift b/macOS/writing-tools/UpdateChecker.swift new file mode 100644 index 0000000..a65c20d --- /dev/null +++ b/macOS/writing-tools/UpdateChecker.swift @@ -0,0 +1,68 @@ +import Foundation +import AppKit + +@Observable +final class UpdateChecker: Sendable { + static let shared = UpdateChecker() + private let currentVersion = 1 // Current app version + private let updateCheckURL = "https://raw.githubusercontent.com/theJayTea/WritingTools/main/macOS/Latest_Version_for_Update_Check.txt" + private let updateDownloadURL = "https://github.com/theJayTea/WritingTools/releases" + + var isCheckingForUpdates = false + var updateAvailable = false + var checkError: String? + + private init() {} + + @MainActor + func checkForUpdates() async { + isCheckingForUpdates = true + checkError = nil + + defer { + isCheckingForUpdates = false + } + + guard let url = URL(string: updateCheckURL) else { + checkError = "Invalid update check URL" + return + } + + do { + let (data, _) = try await URLSession.shared.data(from: url) + + // Print raw data for debugging + if let rawString = String(data: data, encoding: .utf8) { + print("Raw version data: '\(rawString)'") + } + + // Clean up the version string more aggressively + let cleanedString = String(data: data, encoding: .utf8)? + .components(separatedBy: .newlines) + .first? + .trimmingCharacters(in: .whitespacesAndNewlines) + .replacingOccurrences(of: "\n", with: "") + .replacingOccurrences(of: "\r", with: "") + + if let versionString = cleanedString, + !versionString.isEmpty, + let latestVersion = Int(versionString) { + print("Parsed version: \(latestVersion)") + updateAvailable = latestVersion > currentVersion + } else { + checkError = "Invalid version format" + if let cleanedString = cleanedString { + print("Failed to parse version from: '\(cleanedString)'") + } + } + } catch { + checkError = "Failed to check for updates: \(error.localizedDescription)" + } + } + + func openReleasesPage() { + if let url = URL(string: updateDownloadURL) { + NSWorkspace.shared.open(url) + } + } +} From 93cc35993ac45f84b13d06bbf0bc24cb750a8d53 Mon Sep 17 00:00:00 2001 From: Aryamirsepasi Date: Mon, 13 Jan 2025 11:45:11 +0100 Subject: [PATCH 9/9] added version txt --- macOS/Latest_Version_for_Update_Check.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 macOS/Latest_Version_for_Update_Check.txt diff --git a/macOS/Latest_Version_for_Update_Check.txt b/macOS/Latest_Version_for_Update_Check.txt new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/macOS/Latest_Version_for_Update_Check.txt @@ -0,0 +1 @@ +1 \ No newline at end of file