From e87de3a9e5c8dee20d0d5d3a225db332ab9bf9f5 Mon Sep 17 00:00:00 2001 From: Vitalii Parovishnyk <870237+ikorich@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:04:30 +0200 Subject: [PATCH 1/2] updated SPMs --- .../xcshareddata/swiftpm/Package.resolved | 123 ++++++++++++++++++ BifcodePackage/Package.resolved | 105 +++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 Bifcode.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 BifcodePackage/Package.resolved diff --git a/Bifcode.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Bifcode.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..0ead720 --- /dev/null +++ b/Bifcode.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,123 @@ +{ + "originHash" : "12c35fbdbe657f2d43e66c49702894fe4acff2e94879d546a9bedf76b8a0b966", + "pins" : [ + { + "identity" : "codeeditlanguages", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditLanguages.git", + "state" : { + "revision" : "331d5dbc5fc8513be5848fce8a2a312908f36a11", + "version" : "0.1.20" + } + }, + { + "identity" : "codeeditsourceeditor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor", + "state" : { + "revision" : "424453d2232c9912933a3b5a1f3d3df669404ed0", + "version" : "0.15.2" + } + }, + { + "identity" : "codeeditsymbols", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditSymbols.git", + "state" : { + "revision" : "ae69712b08571c4469c2ed5cd38ad9f19439793e", + "version" : "0.2.3" + } + }, + { + "identity" : "codeedittextview", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditTextView.git", + "state" : { + "revision" : "d7ac3f11f22ec2e820187acce8f3a3fb7aa8ddec", + "version" : "0.12.1" + } + }, + { + "identity" : "developersupportstore", + "kind" : "remoteSourceControl", + "location" : "https://github.com/IGRSoft/DeveloperSupportStore", + "state" : { + "revision" : "c9d8f3282a649273fdbf4d3841e45e10a4951ee4", + "version" : "1.0.3" + } + }, + { + "identity" : "rearrange", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/Rearrange", + "state" : { + "revision" : "f1d74e1642956f0300756ad8d1d64e9034857bc3", + "version" : "2.0.0" + } + }, + { + "identity" : "storehelper", + "kind" : "remoteSourceControl", + "location" : "https://github.com/russell-archer/StoreHelper.git", + "state" : { + "revision" : "1c55f49473d847fd5a2f495aabe45f6e5ea3e2ef", + "version" : "2.7.1" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", + "version" : "1.3.0" + } + }, + { + "identity" : "swiftlintplugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/lukepistrol/SwiftLintPlugin", + "state" : { + "revision" : "17c132fe0ce3b4324dc23b575636b223483207b3", + "version" : "0.63.0" + } + }, + { + "identity" : "swifttreesitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/SwiftTreeSitter.git", + "state" : { + "revision" : "08ef81eb8620617b55b08868126707ad72bf754f", + "version" : "0.25.0" + } + }, + { + "identity" : "textformation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/TextFormation", + "state" : { + "revision" : "b1ce9a14bd86042bba4de62236028dc4ce9db6a1", + "version" : "0.9.0" + } + }, + { + "identity" : "textstory", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/TextStory", + "state" : { + "revision" : "91df6fc9bd817f9712331a4a3e826f7bdc823e1d", + "version" : "0.9.1" + } + }, + { + "identity" : "tree-sitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter", + "state" : { + "revision" : "da6fe9beb4f7f67beb75914ca8e0d48ae48d6406", + "version" : "0.25.10" + } + } + ], + "version" : 3 +} diff --git a/BifcodePackage/Package.resolved b/BifcodePackage/Package.resolved new file mode 100644 index 0000000..f1d1567 --- /dev/null +++ b/BifcodePackage/Package.resolved @@ -0,0 +1,105 @@ +{ + "originHash" : "804d0b5514c0fb966aa534f4db8957d2ecd87c6e2c79a0c5e33d6986765da294", + "pins" : [ + { + "identity" : "codeeditlanguages", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditLanguages.git", + "state" : { + "revision" : "331d5dbc5fc8513be5848fce8a2a312908f36a11", + "version" : "0.1.20" + } + }, + { + "identity" : "codeeditsourceeditor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor", + "state" : { + "revision" : "424453d2232c9912933a3b5a1f3d3df669404ed0", + "version" : "0.15.2" + } + }, + { + "identity" : "codeeditsymbols", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditSymbols.git", + "state" : { + "revision" : "ae69712b08571c4469c2ed5cd38ad9f19439793e", + "version" : "0.2.3" + } + }, + { + "identity" : "codeedittextview", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditTextView.git", + "state" : { + "revision" : "d7ac3f11f22ec2e820187acce8f3a3fb7aa8ddec", + "version" : "0.12.1" + } + }, + { + "identity" : "rearrange", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/Rearrange", + "state" : { + "revision" : "f1d74e1642956f0300756ad8d1d64e9034857bc3", + "version" : "2.0.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", + "version" : "1.3.0" + } + }, + { + "identity" : "swiftlintplugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/lukepistrol/SwiftLintPlugin", + "state" : { + "revision" : "9bbc46a4cf8275ceb39334f6276b6b215d67d5d5", + "version" : "0.62.2" + } + }, + { + "identity" : "swifttreesitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/SwiftTreeSitter.git", + "state" : { + "revision" : "08ef81eb8620617b55b08868126707ad72bf754f", + "version" : "0.25.0" + } + }, + { + "identity" : "textformation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/TextFormation", + "state" : { + "revision" : "b1ce9a14bd86042bba4de62236028dc4ce9db6a1", + "version" : "0.9.0" + } + }, + { + "identity" : "textstory", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/TextStory", + "state" : { + "revision" : "91df6fc9bd817f9712331a4a3e826f7bdc823e1d", + "version" : "0.9.1" + } + }, + { + "identity" : "tree-sitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter", + "state" : { + "revision" : "da6fe9beb4f7f67beb75914ca8e0d48ae48d6406", + "version" : "0.25.10" + } + } + ], + "version" : 3 +} From dc68440381b48c813ee547a457922e91e9abed32 Mon Sep 17 00:00:00 2001 From: Vitalii Parovishnyk <870237+ikorich@users.noreply.github.com> Date: Sat, 31 Jan 2026 12:18:24 +0200 Subject: [PATCH 2/2] #28 feat: add copy to clipboard functionality Adds a Copy button next to the Export button that copies the rendered comparison image directly to the system clipboard for easy pasting into Slack, documentation, etc. Changes: - Add .copied case to ExportResult for clipboard feedback - Add copyToClipboard() function using NSPasteboard - Add Copy button in ToolBarView with doc.on.clipboard icon - Update ToastView to show "Copied to clipboard" message --- .../Sources/BifcodeFeature/ContentView.swift | 31 +++++++- .../BifcodeFeature/Views/ToastView.swift | 67 ++++++++++++++--- .../BifcodeFeature/Views/ToolBarView.swift | 71 +++++++++++++------ 3 files changed, 138 insertions(+), 31 deletions(-) diff --git a/BifcodePackage/Sources/BifcodeFeature/ContentView.swift b/BifcodePackage/Sources/BifcodeFeature/ContentView.swift index bb51890..c42f493 100644 --- a/BifcodePackage/Sources/BifcodeFeature/ContentView.swift +++ b/BifcodePackage/Sources/BifcodeFeature/ContentView.swift @@ -2,7 +2,7 @@ // ContentView.swift // // Created on 17.12.2025. -// Copyright © 2025 IGR Soft. All rights reserved. +// Copyright © 2026 IGR Soft. All rights reserved. // import AppKit @@ -128,6 +128,7 @@ public struct ContentView: View { dontTitleSetting: $dontTitleSetting, isExportDisabled: isCodeEmpty, onExport: { Task(operation: exportImage) }, + onCopyToClipboard: { Task(operation: copyToClipboard) }, onLanguageChange: { viewModel.setLanguage($0) } ) @@ -262,6 +263,34 @@ public struct ContentView: View { } } + /// Copies the current code panels as an image to the clipboard. + /// + /// This method uses the same rendering pipeline as ``exportImage()`` + /// but copies the result to the system clipboard instead of saving to disk. + /// + /// > Note: Copy is disabled when both code panels are empty. + @MainActor + private func copyToClipboard() async { + guard let nsImage = await renderExportViewToImage() else { + withAnimation(reduceMotion ? nil : .easeInOut(duration: 0.25)) { + exportResult = .failure(ExportError.conversionFailed) + } + return + } + + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + let success = pasteboard.writeObjects([nsImage]) + + withAnimation(reduceMotion ? nil : .easeInOut(duration: 0.25)) { + if success { + exportResult = .copied + } else { + exportResult = .failure(ExportError.conversionFailed) + } + } + } + /// Renders the export view to an NSImage using an offscreen window. /// /// This method uses an offscreen `NSWindow` technique because `ImageRenderer` diff --git a/BifcodePackage/Sources/BifcodeFeature/Views/ToastView.swift b/BifcodePackage/Sources/BifcodeFeature/Views/ToastView.swift index 66aec0f..7ae1e8c 100644 --- a/BifcodePackage/Sources/BifcodeFeature/Views/ToastView.swift +++ b/BifcodePackage/Sources/BifcodeFeature/Views/ToastView.swift @@ -2,7 +2,7 @@ // ToastView.swift // // Created on 23.12.2025. -// Copyright © 2025 IGR Soft. All rights reserved. +// Copyright © 2026 IGR Soft. All rights reserved. // import AppKit @@ -16,21 +16,32 @@ public enum ExportResult: Sendable { /// Export completed successfully with the file saved at the given URL. case success(URL) + /// Image copied to clipboard successfully. + case copied + /// Export failed with the given error. case failure(Error) - /// Whether the export was successful. + /// Whether the export was successful (saved or copied). var isSuccess: Bool { - if case .success = self { return true } - return false + switch self { + case .success, .copied: true + case .failure: false + } } - /// The exported file URL if successful, nil otherwise. + /// The exported file URL if saved to disk, nil for copied or failed. var fileURL: URL? { if case .success(let url) = self { return url } return nil } + /// Whether the result is a clipboard copy operation. + var isCopied: Bool { + if case .copied = self { return true } + return false + } + /// The error if failed, nil otherwise. var error: Error? { if case .failure(let error) = self { return error } @@ -93,14 +104,14 @@ public struct ToastView: View { public var body: some View { Button(action: handleTap) { HStack(spacing: 8) { - Image(systemName: result.isSuccess ? "checkmark.circle.fill" : "xmark.circle.fill") + Image(systemName: iconName) .font(.system(size: 16, weight: .medium)) Text(message) .font(.subheadline.weight(.medium)) .lineLimit(1) - if result.isSuccess { + if result.fileURL != nil { Image(systemName: "arrow.right.circle") .font(.system(size: 14)) .foregroundStyle(.secondary) @@ -115,7 +126,7 @@ public struct ToastView: View { } .buttonStyle(.plain) .accessibilityLabel(accessibilityLabel) - .accessibilityHint(result.isSuccess ? "Double-tap to reveal in Finder" : "Double-tap to dismiss") + .accessibilityHint(accessibilityHint) .accessibilityAddTraits(result.isSuccess ? .isButton : []) .transition(.move(edge: .top).combined(with: .opacity)) .task { @@ -126,8 +137,21 @@ public struct ToastView: View { // MARK: - Computed Properties + private var iconName: String { + switch result { + case .success: + "checkmark.circle.fill" + case .copied: + "doc.on.clipboard.fill" + case .failure: + "xmark.circle.fill" + } + } + private var message: String { - if result.isSuccess { + if result.isCopied { + "Copied to clipboard" + } else if result.isSuccess { "Exported successfully" } else if let error = result.error { error.localizedDescription @@ -145,7 +169,9 @@ public struct ToastView: View { } private var accessibilityLabel: String { - if result.isSuccess { + if result.isCopied { + "Image copied to clipboard." + } else if result.isSuccess { "Export successful. Click to reveal file in Finder." } else if let error = result.error { "Export failed: \(error.localizedDescription)" @@ -154,6 +180,14 @@ public struct ToastView: View { } } + private var accessibilityHint: String { + if result.fileURL != nil { + "Double-tap to reveal in Finder" + } else { + "Double-tap to dismiss" + } + } + // MARK: - Actions private func handleTap() { @@ -186,6 +220,19 @@ public struct ToastView: View { .background(Color.gray.opacity(0.2)) } +#Preview("Copied Toast") { + VStack { + Spacer() + ToastView( + result: .copied, + onDismiss: {} + ) + Spacer() + } + .frame(width: 400, height: 200) + .background(Color.gray.opacity(0.2)) +} + #Preview("Error Toast") { VStack { Spacer() diff --git a/BifcodePackage/Sources/BifcodeFeature/Views/ToolBarView.swift b/BifcodePackage/Sources/BifcodeFeature/Views/ToolBarView.swift index a19742e..d5d0ef4 100644 --- a/BifcodePackage/Sources/BifcodeFeature/Views/ToolBarView.swift +++ b/BifcodePackage/Sources/BifcodeFeature/Views/ToolBarView.swift @@ -2,7 +2,7 @@ // ToolBarView.swift // // Created on 17.12.2025. -// Copyright © 2025 IGR Soft. All rights reserved. +// Copyright © 2026 IGR Soft. All rights reserved. // import CodeEditLanguages @@ -96,6 +96,7 @@ public struct ToolBarView: View { // MARK: - Callbacks var onExport: () -> Void + var onCopyToClipboard: () -> Void var onLanguageChange: (CodeLanguage) -> Void var isExportDisabled: Bool @@ -109,6 +110,7 @@ public struct ToolBarView: View { /// - isExportDisabled: Whether the export button should be disabled. /// Set to `true` when both code panels are empty. /// - onExport: Closure called when the export button is tapped. + /// - onCopyToClipboard: Closure called when the copy button is tapped. /// - onLanguageChange: Closure called when the language selection changes, /// providing the newly selected ``CodeLanguage``. /// @@ -118,6 +120,7 @@ public struct ToolBarView: View { /// dontTitleSetting: $dontTitle, /// isExportDisabled: viewModel.doPanel.code.isEmpty, /// onExport: { Task { await exportImage() } }, + /// onCopyToClipboard: { Task { await copyToClipboard() } }, /// onLanguageChange: { viewModel.setLanguage($0) } /// ) /// ``` @@ -126,12 +129,14 @@ public struct ToolBarView: View { dontTitleSetting: Binding, isExportDisabled: Bool = false, onExport: @escaping () -> Void, + onCopyToClipboard: @escaping () -> Void, onLanguageChange: @escaping (CodeLanguage) -> Void ) { _doTitleSetting = doTitleSetting _dontTitleSetting = dontTitleSetting self.isExportDisabled = isExportDisabled self.onExport = onExport + self.onCopyToClipboard = onCopyToClipboard self.onLanguageChange = onLanguageChange } @@ -513,6 +518,7 @@ public struct ToolBarView: View { // MARK: - Export Button @State private var isExportButtonHovered = false + @State private var isCopyButtonHovered = false @State private var isStorePresented = false /// Tracks whether the user has made any purchase (subscription or tip). @@ -524,25 +530,48 @@ public struct ToolBarView: View { private var exportButton: some View { VStack(spacing: 12) { - // Primary Export Action - HIG: Use .prominent for key actions - Button { onExport() } label: { - Image(systemName: "square.and.arrow.up") - .font(.system(size: 22, weight: .medium)) - .frame(width: 48, height: 48) - } - .buttonStyle(.borderedProminent) - .clipShape(Circle()) - .scaleEffect(isExportButtonHovered && !isExportDisabled ? 1.05 : 1.0) - .animation(reduceMotion ? nil : .easeInOut(duration: 0.15), value: isExportButtonHovered) - .onHover { hovering in - isExportButtonHovered = hovering + // Primary Actions Row - Export and Copy + HStack(spacing: 12) { + // Export Button - HIG: Use .prominent for key actions + Button { onExport() } label: { + Image(systemName: "square.and.arrow.up") + .font(.system(size: 22, weight: .medium)) + .frame(width: 48, height: 48) + } + .buttonStyle(.borderedProminent) + .clipShape(Circle()) + .scaleEffect(isExportButtonHovered && !isExportDisabled ? 1.05 : 1.0) + .animation(reduceMotion ? nil : .easeInOut(duration: 0.15), value: isExportButtonHovered) + .onHover { hovering in + isExportButtonHovered = hovering + } + .disabled(isExportDisabled) + .help("Export as PNG") + .accessibilityLabel("Export as PNG") + .accessibilityHint(isExportDisabled + ? "Disabled: Add code to enable export" + : "Save comparison image to selected folder") + + // Copy to Clipboard Button + Button { onCopyToClipboard() } label: { + Image(systemName: "doc.on.clipboard") + .font(.system(size: 20, weight: .medium)) + .frame(width: 48, height: 48) + } + .buttonStyle(.bordered) + .clipShape(Circle()) + .scaleEffect(isCopyButtonHovered && !isExportDisabled ? 1.05 : 1.0) + .animation(reduceMotion ? nil : .easeInOut(duration: 0.15), value: isCopyButtonHovered) + .onHover { hovering in + isCopyButtonHovered = hovering + } + .disabled(isExportDisabled) + .help("Copy to clipboard") + .accessibilityLabel("Copy to clipboard") + .accessibilityHint(isExportDisabled + ? "Disabled: Add code to enable copy" + : "Copy comparison image to clipboard for pasting") } - .disabled(isExportDisabled) - .help("Export as PNG") - .accessibilityLabel("Export as PNG") - .accessibilityHint(isExportDisabled - ? "Disabled: Add code to enable export" - : "Save comparison image to selected folder") // Secondary Actions VStack(spacing: 8) { @@ -561,7 +590,7 @@ public struct ToolBarView: View { .padding(.top, 32) } } - .frame(minWidth: 80) + .frame(minWidth: 120) } private var storeButton: some View { @@ -636,6 +665,7 @@ public struct ToolBarView: View { dontTitleSetting: .constant("2"), isExportDisabled: false, onExport: {}, + onCopyToClipboard: {}, onLanguageChange: { _ in } ) } @@ -646,6 +676,7 @@ public struct ToolBarView: View { dontTitleSetting: .constant("2"), isExportDisabled: true, onExport: {}, + onCopyToClipboard: {}, onLanguageChange: { _ in } ) }