Skip to content

Commit 1fc9090

Browse files
authored
Merge pull request #169 from theJayTea/feature/arya-macOS
version 3.1
2 parents 2c94651 + c72e1eb commit 1fc9090

29 files changed

Lines changed: 1719 additions & 707 deletions
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3
1+
3.1

macOS/writing-tools.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@
444444
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
445445
CODE_SIGN_STYLE = Automatic;
446446
COMBINE_HIDPI_IMAGES = YES;
447-
CURRENT_PROJECT_VERSION = 4;
447+
CURRENT_PROJECT_VERSION = 6;
448448
DEAD_CODE_STRIPPING = YES;
449449
DEVELOPMENT_ASSET_PATHS = "\"writing-tools/Preview Content\"";
450450
DEVELOPMENT_TEAM = MK2V998W66;
@@ -460,7 +460,7 @@
460460
"@executable_path/../Frameworks",
461461
);
462462
MACOSX_DEPLOYMENT_TARGET = 14.0;
463-
MARKETING_VERSION = 3.0;
463+
MARKETING_VERSION = 3.1;
464464
PRODUCT_BUNDLE_IDENTIFIER = "com.aryamirsepasi.writing-tools";
465465
PRODUCT_NAME = "$(TARGET_NAME)";
466466
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -479,7 +479,7 @@
479479
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
480480
CODE_SIGN_STYLE = Automatic;
481481
COMBINE_HIDPI_IMAGES = YES;
482-
CURRENT_PROJECT_VERSION = 4;
482+
CURRENT_PROJECT_VERSION = 6;
483483
DEAD_CODE_STRIPPING = YES;
484484
DEVELOPMENT_ASSET_PATHS = "\"writing-tools/Preview Content\"";
485485
DEVELOPMENT_TEAM = MK2V998W66;
@@ -495,7 +495,7 @@
495495
"@executable_path/../Frameworks",
496496
);
497497
MACOSX_DEPLOYMENT_TARGET = 14.0;
498-
MARKETING_VERSION = 3.0;
498+
MARKETING_VERSION = 3.1;
499499
PRODUCT_BUNDLE_IDENTIFIER = "com.aryamirsepasi.writing-tools";
500500
PRODUCT_NAME = "$(TARGET_NAME)";
501501
PROVISIONING_PROFILE_SPECIFIER = "";

macOS/writing-tools/AppState.swift

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ class AppState: ObservableObject {
1717
@Published var previousApplication: NSRunningApplication?
1818
@Published var selectedImages: [Data] = [] // Store selected image data
1919

20+
// Command management
21+
@Published var commandManager = CommandManager()
22+
@Published var customCommandsManager = CustomCommandsManager()
23+
2024
// Current provider with UI binding support
2125
@Published private(set) var currentProvider: String
2226

@@ -77,6 +81,12 @@ class AppState: ObservableObject {
7781
if asettings.openAIApiKey.isEmpty && asettings.geminiApiKey.isEmpty && asettings.mistralApiKey.isEmpty {
7882
print("Warning: No API keys configured.")
7983
}
84+
85+
// Perform migration from old system to new CommandManager if needed
86+
MigrationHelper.shared.migrateIfNeeded(
87+
commandManager: commandManager,
88+
customCommandsManager: customCommandsManager
89+
)
8090
}
8191

8292
// For Gemini changes
@@ -134,4 +144,89 @@ class AppState: ObservableObject {
134144
let config = OllamaConfig(baseURL: baseURL, model: model, keepAlive: keepAlive)
135145
ollamaProvider = OllamaProvider(config: config)
136146
}
147+
148+
// Process a command (unified method for all command types)
149+
func processCommand(_ command: CommandModel) {
150+
guard !selectedText.isEmpty else { return }
151+
152+
isProcessing = true
153+
154+
Task {
155+
do {
156+
let prompt = command.prompt
157+
let result = try await activeProvider.processText(
158+
systemPrompt: prompt,
159+
userPrompt: selectedText,
160+
images: []
161+
)
162+
163+
// Determine what to do with the result based on command settings
164+
if command.useResponseWindow {
165+
// Display in response window
166+
let window = ResponseWindow(
167+
title: "\(command.name) Result",
168+
content: result,
169+
selectedText: selectedText,
170+
option: nil // Using nil since this is using the generic CommandModel
171+
)
172+
173+
WindowManager.shared.addResponseWindow(window)
174+
window.makeKeyAndOrderFront(nil)
175+
window.orderFrontRegardless()
176+
} else {
177+
// Replace selected text by setting clipboard and pasting
178+
NSPasteboard.general.clearContents()
179+
NSPasteboard.general.setString(result, forType: .string)
180+
181+
// Reactivate previous application and paste
182+
if let previousApp = previousApplication {
183+
previousApp.activate()
184+
185+
// Wait briefly for activation then paste once
186+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
187+
self.simulatePaste()
188+
}
189+
}
190+
}
191+
} catch {
192+
// Handle error
193+
print("Error processing command: \(error)")
194+
}
195+
196+
isProcessing = false
197+
}
198+
}
199+
200+
// Helper method to replace selected text
201+
func replaceSelectedText(with newText: String) {
202+
NSPasteboard.general.clearContents()
203+
NSPasteboard.general.setString(newText, forType: .string)
204+
205+
// Reactivate previous application and paste
206+
if let previousApp = previousApplication {
207+
previousApp.activate()
208+
209+
// Wait briefly for activation then paste once
210+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
211+
self.simulatePaste()
212+
}
213+
}
214+
}
215+
216+
// Simulate paste command
217+
private func simulatePaste() {
218+
guard let source = CGEventSource(stateID: .hidSystemState) else { return }
219+
220+
// Create a Command + V key down event
221+
let keyDown = CGEvent(keyboardEventSource: source, virtualKey: 0x09, keyDown: true)
222+
keyDown?.flags = .maskCommand
223+
224+
// Create a Command + V key up event
225+
let keyUp = CGEvent(keyboardEventSource: source, virtualKey: 0x09, keyDown: false)
226+
keyUp?.flags = .maskCommand
227+
228+
// Post the events to the HID event system
229+
keyDown?.post(tap: .cghidEventTap)
230+
keyUp?.post(tap: .cghidEventTap)
231+
}
137232
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import SwiftUI
2+
3+
struct CommandButton: View {
4+
let command: CommandModel
5+
let isEditing: Bool
6+
let isLoading: Bool
7+
let onTap: () -> Void
8+
let onEdit: () -> Void
9+
let onDelete: () -> Void
10+
11+
var body: some View {
12+
ZStack {
13+
// Main button wrapper
14+
Button(action: {
15+
if !isEditing && !isLoading {
16+
onTap()
17+
}
18+
}) {
19+
HStack {
20+
// Leave space for the delete button if in edit mode
21+
if isEditing {
22+
Color.clear
23+
.frame(width: 10, height: 16)
24+
}
25+
26+
HStack(spacing: 4) {
27+
Image(systemName: command.icon)
28+
Text(command.name)
29+
.lineLimit(1)
30+
.truncationMode(.tail)
31+
}
32+
33+
// Leave space for the edit button if in edit mode
34+
if isEditing {
35+
Color.clear
36+
.frame(width: 10, height: 16)
37+
}
38+
}
39+
.frame(maxWidth: 140)
40+
.padding()
41+
.background(Color(.controlBackgroundColor))
42+
.cornerRadius(8)
43+
}
44+
.buttonStyle(LoadingButtonStyle(isLoading: isLoading))
45+
.disabled(isLoading || isEditing)
46+
47+
// Overlay edit controls when in edit mode
48+
if isEditing {
49+
HStack {
50+
Button(action: onDelete) {
51+
Image(systemName: "minus.circle")
52+
.foregroundColor(.red)
53+
.padding(8)
54+
.contentShape(Rectangle())
55+
}
56+
.buttonStyle(.plain)
57+
58+
Spacer()
59+
60+
Button(action: onEdit) {
61+
Image(systemName: "pencil.circle")
62+
.foregroundColor(.blue)
63+
.padding(8)
64+
.contentShape(Rectangle())
65+
}
66+
.buttonStyle(.plain)
67+
}
68+
.frame(maxWidth: 140)
69+
.padding(.horizontal, 8)
70+
}
71+
}
72+
}
73+
}
74+
75+
struct LoadingButtonStyle: ButtonStyle {
76+
var isLoading: Bool
77+
78+
func makeBody(configuration: Configuration) -> some View {
79+
configuration.label
80+
.opacity(isLoading ? 0.5 : 1.0)
81+
.overlay(
82+
Group {
83+
if isLoading {
84+
ProgressView()
85+
.progressViewStyle(CircularProgressViewStyle())
86+
}
87+
}
88+
)
89+
}
90+
}
91+
92+
#Preview {
93+
VStack {
94+
CommandButton(
95+
command: CommandModel.proofread,
96+
isEditing: false,
97+
isLoading: false,
98+
onTap: {},
99+
onEdit: {},
100+
onDelete: {}
101+
)
102+
103+
CommandButton(
104+
command: CommandModel.proofread,
105+
isEditing: true,
106+
isLoading: false,
107+
onTap: {},
108+
onEdit: {},
109+
onDelete: {}
110+
)
111+
}
112+
}

0 commit comments

Comments
 (0)