Skip to content

Commit

Permalink
Merge pull request #62 from Aryamirsepasi/main
Browse files Browse the repository at this point in the history
Beta 4
  • Loading branch information
theJayTea authored Dec 3, 2024
2 parents 9c7a15b + 732ff0f commit 3a3aaba
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 93 deletions.
4 changes: 2 additions & 2 deletions macOS/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ Core functionality works well, and it is still an ongoing work in progress.

## Working Features
- All of the tools, including the new response windows and the manual chat option.
- Input Window even when no text is selected
- Gemini, OpenAI and Local LLM Support.
- The Gradient Theme (Dark Mode and Light Mode are supported).
- Initial Setup, Settings, and About pages.

---

## Not Yet Available
- Opening the Input Window when no text is selected
- More refined positioning logic for the popup window to follow the cursor correctly.
- All of the original port's features are now available; however, more optimizations and improvements are coming soon.

---

Expand Down
120 changes: 87 additions & 33 deletions macOS/writing-tools/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,19 @@ import HotKey
import Carbon.HIToolbox

class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
var statusBarItem: NSStatusItem!
private static var sharedStatusItem: NSStatusItem?
var statusBarItem: NSStatusItem! {
get {
if AppDelegate.sharedStatusItem == nil {
AppDelegate.sharedStatusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
configureStatusBarItem()
}
return AppDelegate.sharedStatusItem
}
set {
AppDelegate.sharedStatusItem = newValue
}
}
var hotKey: HotKey?
let appState = AppState.shared
private var settingsWindow: NSWindow?
Expand All @@ -14,20 +26,69 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
private let windowAccessQueue = DispatchQueue(label: "com.example.writingtools.windowQueue")

func applicationDidFinishLaunching(_ notification: Notification) {
setupMenuBar()
setupHotKey()

if !UserDefaults.standard.bool(forKey: "has_completed_onboarding") {
showOnboarding()
DispatchQueue.main.async { [weak self] in
self?.setupMenuBar()
self?.setupHotKey()

if self?.statusBarItem == nil {
self?.recreateStatusBarItem()
}

if !UserDefaults.standard.bool(forKey: "has_completed_onboarding") {
self?.showOnboarding()
}

self?.requestAccessibilityPermissions()
}

requestAccessibilityPermissions()
}

func applicationWillTerminate(_ notification: Notification) {
WindowManager.shared.cleanupWindows()
}

private func recreateStatusBarItem() {
AppDelegate.sharedStatusItem = nil
_ = self.statusBarItem
}

private func configureStatusBarItem() {
guard let button = statusBarItem?.button else { return }
button.image = NSImage(systemSymbolName: "pencil.circle", accessibilityDescription: "Writing Tools")
}

private func setupMenuBar() {
guard let statusBarItem = self.statusBarItem else {
print("Failed to create status bar item")
return
}

let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Settings", action: #selector(showSettings), keyEquivalent: ","))
menu.addItem(NSMenuItem(title: "About", action: #selector(showAbout), keyEquivalent: "i"))
menu.addItem(NSMenuItem(title: "Reset App", action: #selector(resetApp), keyEquivalent: "r"))
menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))

statusBarItem.menu = menu
}

@objc private func resetApp() {
hotKey = nil
WindowManager.shared.cleanupWindows()

recreateStatusBarItem()
setupMenuBar()

setupHotKey()

let alert = NSAlert()
alert.messageText = "App Reset Complete"
alert.informativeText = "The app has been reset. If you're still experiencing issues, try restarting the app."
alert.alertStyle = .informational
alert.addButton(withTitle: "OK")
alert.runModal()
}

private func requestAccessibilityPermissions() {
let trusted = AXIsProcessTrusted()
if !trusted {
Expand All @@ -44,25 +105,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
}
}

private func setupMenuBar() {
statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)

if let button = statusBarItem.button {
button.image = NSImage(systemSymbolName: "pencil.circle", accessibilityDescription: "Writing Tools")
}

let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Settings", action: #selector(showSettings), keyEquivalent: ","))
menu.addItem(NSMenuItem(title: "About", action: #selector(showAbout), keyEquivalent: ""))
menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))

statusBarItem.menu = menu
}

private func setupHotKey() {
updateHotKey()

NotificationCenter.default.removeObserver(self, name: UserDefaults.didChangeNotification, object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(shortcutChanged),
Expand All @@ -72,12 +118,15 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
}

@objc private func shortcutChanged() {
if UserDefaults.standard.string(forKey: "shortcut") != nil {
updateHotKey()
DispatchQueue.main.async { [weak self] in
if UserDefaults.standard.string(forKey: "shortcut") != nil {
self?.updateHotKey()
}
}
}

private func updateHotKey() {
// Remove existing hotkey first
hotKey = nil

let shortcutText = UserDefaults.standard.string(forKey: "shortcut") ?? "⌥ Space"
Expand All @@ -103,8 +152,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
}
}

guard keyCode != 0 else { return }

guard keyCode != 0 else {
print("Invalid key code, resetting to default shortcut")
UserDefaults.standard.set("⌥ Space", forKey: "shortcut")
updateHotKey()
return
}

hotKey = HotKey(keyCombo: KeyCombo(carbonKeyCode: keyCode, carbonModifiers: modifiers.carbonFlags))
hotKey?.keyDownHandler = { [weak self] in
DispatchQueue.main.async {
Expand Down Expand Up @@ -214,17 +268,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
pasteboard.setString(oldContents, forType: .string)
}

guard !selectedText.isEmpty else {
print("No text selected.")
return
}

// Create window even if no text is selected
let window = PopupWindow(appState: self.appState)
window.delegate = self

self.appState.selectedText = selectedText
self.popupWindow = window

if selectedText.isEmpty {
window.setContentSize(NSSize(width: 400, height: 100))
}

window.positionNearMouse()
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless()
Expand Down
2 changes: 1 addition & 1 deletion macOS/writing-tools/UI/AboutView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ struct AboutView: View {

Divider()

Text("Version: 5.0 Beta 3")
Text("Version: Beta 4 (Based on Windows Port version 5.0)")
.font(.caption)

Button("Check for Updates") {
Expand Down
1 change: 0 additions & 1 deletion macOS/writing-tools/UI/BackgroundModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ struct WindowBackground: ViewModifier {
Color(.windowBackgroundColor)
}
}
.clipShape(RoundedRectangle(cornerRadius: 12))
)
}
}
Expand Down
4 changes: 3 additions & 1 deletion macOS/writing-tools/UI/PopupView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ struct PopupView: View {
.padding(.top, 8)
.padding(.trailing, 8)
}

// Custom input with send button
HStack(spacing: 8) {
TextField(
appState.selectedText.isEmpty ? "Please enter an instruction..." : "Describe your change...",
appState.selectedText.isEmpty ? "Enter your instruction..." : "Describe your change...",
text: $customText
)
.textFieldStyle(RoundedBorderTextFieldStyle())
Expand All @@ -45,6 +46,7 @@ struct PopupView: View {
}
.padding(.horizontal)

// Only show options grid if text is selected
if !appState.selectedText.isEmpty {
LazyVGrid(columns: [
GridItem(.flexible()),
Expand Down
35 changes: 23 additions & 12 deletions macOS/writing-tools/UI/PopupWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ class PopupWindow: NSWindow {
let hostingView = NSHostingView(rootView: popupView)
contentView = hostingView
retainedHostingView = hostingView

if appState.selectedText.isEmpty {
setContentSize(NSSize(width: 400, height: 100))
}
}

private func setupTrackingArea() {
Expand Down Expand Up @@ -124,36 +128,43 @@ class PopupWindow: NSWindow {


// MARK: - Window Positioning

// Find the screen where the mouse cursor is located
func screenAt(point: NSPoint) -> NSScreen? {
for screen in NSScreen.screens {
if screen.frame.contains(point) {
return screen
}
}
return nil
}

func positionNearMouse() {
guard let screen = NSScreen.main else { return }

// Get the mouse location and screen dimensions
let mouseLocation = NSEvent.mouseLocation

guard let screen = screenAt(point: mouseLocation) else { return }

let screenFrame = screen.visibleFrame
let padding: CGFloat = 10
var windowFrame = frame

windowFrame.origin.x = mouseLocation.x + padding
windowFrame.origin.y = screenFrame.maxY - mouseLocation.y - windowFrame.height - padding
windowFrame.origin.y = mouseLocation.y - windowFrame.height - padding

if windowFrame.maxX > screenFrame.maxX {
windowFrame.origin.x = mouseLocation.x - windowFrame.width - padding
}

if windowFrame.minY < screenFrame.minY {
windowFrame.origin.y = screenFrame.maxY - mouseLocation.y + padding
windowFrame.origin.y = mouseLocation.y + padding
}

windowFrame.origin.x = max(screenFrame.minX + padding,
min(windowFrame.origin.x,
screenFrame.maxX - windowFrame.width - padding))
windowFrame.origin.y = max(screenFrame.minY + padding,
min(windowFrame.origin.y,
screenFrame.maxY - windowFrame.height - padding))
windowFrame.origin.x = max(screenFrame.minX + padding, min(windowFrame.origin.x, screenFrame.maxX - windowFrame.width - padding))
windowFrame.origin.y = max(screenFrame.minY + padding, min(windowFrame.origin.y, screenFrame.maxY - windowFrame.height - padding))

// Apply the calculated frame
setFrame(windowFrame, display: true)
}

}

extension PopupWindow: NSWindowDelegate {
Expand Down
60 changes: 42 additions & 18 deletions macOS/writing-tools/UI/ResponseView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,43 @@ import MarkdownUI
final class ResponseViewModel: ObservableObject {
@Published var content: String
@Published var fontSize: CGFloat = 14

@Published var showCopyConfirmation: Bool = false

let selectedText: String
let option: WritingOption

init(content: String, selectedText: String, option: WritingOption) {
self.content = content
self.selectedText = selectedText
self.option = option
}

func regenerateContent() async {
do {
let result = try await AppState.shared.activeProvider.processText(
systemPrompt: option.systemPrompt,
userPrompt: selectedText
)
await MainActor.run {
self.content = result
}
} catch {
print("Error regenerating content: \(error.localizedDescription)")
do {
let result = try await AppState.shared.activeProvider.processText(
systemPrompt: option.systemPrompt,
userPrompt: selectedText
)
await MainActor.run {
self.content = result
}
} catch {
print("Error regenerating content: \(error.localizedDescription)")
}
}

func copyContent() {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(content, forType: .string)

// Show confirmation
showCopyConfirmation = true

// Hide confirmation after 2 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.showCopyConfirmation = false
}
}
}

/// Main ResponseView
Expand Down Expand Up @@ -54,12 +68,22 @@ struct ResponseView: View {
}

HStack {
Button(action: {
Task {
await viewModel.regenerateContent()
HStack(spacing: 12) {
Button(action: {
Task {
await viewModel.regenerateContent()
}
}) {
Label("Regenerate", systemImage: "arrow.clockwise")
}

Button(action: {
viewModel.copyContent()
}) {
Label(viewModel.showCopyConfirmation ? "Copied!" : "Copy",
systemImage: viewModel.showCopyConfirmation ? "checkmark" : "doc.on.doc")
}
}) {
Label("Regenerate", systemImage: "arrow.clockwise")
.animation(.easeInOut, value: viewModel.showCopyConfirmation)
}

Spacer()
Expand Down
Loading

0 comments on commit 3a3aaba

Please sign in to comment.