Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion macOS/Latest_Version_for_Update_Check.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3
3.1
8 changes: 4 additions & 4 deletions macOS/writing-tools.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 6;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"writing-tools/Preview Content\"";
DEVELOPMENT_TEAM = MK2V998W66;
Expand All @@ -460,7 +460,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 3.0;
MARKETING_VERSION = 3.1;
PRODUCT_BUNDLE_IDENTIFIER = "com.aryamirsepasi.writing-tools";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand All @@ -479,7 +479,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 6;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"writing-tools/Preview Content\"";
DEVELOPMENT_TEAM = MK2V998W66;
Expand All @@ -495,7 +495,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 3.0;
MARKETING_VERSION = 3.1;
PRODUCT_BUNDLE_IDENTIFIER = "com.aryamirsepasi.writing-tools";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down
95 changes: 95 additions & 0 deletions macOS/writing-tools/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ class AppState: ObservableObject {
@Published var previousApplication: NSRunningApplication?
@Published var selectedImages: [Data] = [] // Store selected image data

// Command management
@Published var commandManager = CommandManager()
@Published var customCommandsManager = CustomCommandsManager()

// Current provider with UI binding support
@Published private(set) var currentProvider: String

Expand Down Expand Up @@ -77,6 +81,12 @@ class AppState: ObservableObject {
if asettings.openAIApiKey.isEmpty && asettings.geminiApiKey.isEmpty && asettings.mistralApiKey.isEmpty {
print("Warning: No API keys configured.")
}

// Perform migration from old system to new CommandManager if needed
MigrationHelper.shared.migrateIfNeeded(
commandManager: commandManager,
customCommandsManager: customCommandsManager
)
}

// For Gemini changes
Expand Down Expand Up @@ -134,4 +144,89 @@ class AppState: ObservableObject {
let config = OllamaConfig(baseURL: baseURL, model: model, keepAlive: keepAlive)
ollamaProvider = OllamaProvider(config: config)
}

// Process a command (unified method for all command types)
func processCommand(_ command: CommandModel) {
guard !selectedText.isEmpty else { return }

isProcessing = true

Task {
do {
let prompt = command.prompt
let result = try await activeProvider.processText(
systemPrompt: prompt,
userPrompt: selectedText,
images: []
)

// Determine what to do with the result based on command settings
if command.useResponseWindow {
// Display in response window
let window = ResponseWindow(
title: "\(command.name) Result",
content: result,
selectedText: selectedText,
option: nil // Using nil since this is using the generic CommandModel
)

WindowManager.shared.addResponseWindow(window)
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless()
} else {
// Replace selected text by setting clipboard and pasting
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(result, forType: .string)

// Reactivate previous application and paste
if let previousApp = previousApplication {
previousApp.activate()

// Wait briefly for activation then paste once
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.simulatePaste()
}
}
}
} catch {
// Handle error
print("Error processing command: \(error)")
}

isProcessing = false
}
}

// Helper method to replace selected text
func replaceSelectedText(with newText: String) {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(newText, forType: .string)

// Reactivate previous application and paste
if let previousApp = previousApplication {
previousApp.activate()

// Wait briefly for activation then paste once
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.simulatePaste()
}
}
}

// Simulate paste command
private func simulatePaste() {
guard let source = CGEventSource(stateID: .hidSystemState) else { return }

// Create a Command + V key down event
let keyDown = CGEvent(keyboardEventSource: source, virtualKey: 0x09, keyDown: true)
keyDown?.flags = .maskCommand

// Create a Command + V key up event
let keyUp = CGEvent(keyboardEventSource: source, virtualKey: 0x09, keyDown: false)
keyUp?.flags = .maskCommand

// Post the events to the HID event system
keyDown?.post(tap: .cghidEventTap)
keyUp?.post(tap: .cghidEventTap)
}
}
112 changes: 112 additions & 0 deletions macOS/writing-tools/Commands/CommandButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import SwiftUI

struct CommandButton: View {
let command: CommandModel
let isEditing: Bool
let isLoading: Bool
let onTap: () -> Void
let onEdit: () -> Void
let onDelete: () -> Void

var body: some View {
ZStack {
// Main button wrapper
Button(action: {
if !isEditing && !isLoading {
onTap()
}
}) {
HStack {
// Leave space for the delete button if in edit mode
if isEditing {
Color.clear
.frame(width: 10, height: 16)
}

HStack(spacing: 4) {
Image(systemName: command.icon)
Text(command.name)
.lineLimit(1)
.truncationMode(.tail)
}

// Leave space for the edit button if in edit mode
if isEditing {
Color.clear
.frame(width: 10, height: 16)
}
}
.frame(maxWidth: 140)
.padding()
.background(Color(.controlBackgroundColor))
.cornerRadius(8)
}
.buttonStyle(LoadingButtonStyle(isLoading: isLoading))
.disabled(isLoading || isEditing)

// Overlay edit controls when in edit mode
if isEditing {
HStack {
Button(action: onDelete) {
Image(systemName: "minus.circle")
.foregroundColor(.red)
.padding(8)
.contentShape(Rectangle())
}
.buttonStyle(.plain)

Spacer()

Button(action: onEdit) {
Image(systemName: "pencil.circle")
.foregroundColor(.blue)
.padding(8)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
.frame(maxWidth: 140)
.padding(.horizontal, 8)
}
}
}
}

struct LoadingButtonStyle: ButtonStyle {
var isLoading: Bool

func makeBody(configuration: Configuration) -> some View {
configuration.label
.opacity(isLoading ? 0.5 : 1.0)
.overlay(
Group {
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
}
}
)
}
}

#Preview {
VStack {
CommandButton(
command: CommandModel.proofread,
isEditing: false,
isLoading: false,
onTap: {},
onEdit: {},
onDelete: {}
)

CommandButton(
command: CommandModel.proofread,
isEditing: true,
isLoading: false,
onTap: {},
onEdit: {},
onDelete: {}
)
}
}
Loading